iOSアプリ内課金をテストし尽くす〜第3回 paymentQueue関数(Interrupted Purchase、Ask to Buy)〜[Xcode12/iOS14]
NTTレゾナントテクノロジー アジャイルデザイン部の西添です。最近、iOSアプリのアプリ内課金(In-App Purchase)を実装するために調査と実験をしていました。アプリ内課金の実装方法の解説はインターネット上にたくさんありますが、様々な場面でStoreKitがどのように振る舞うのかを紹介したサイトは見かけません。そこで、iOSアプリ内課金をStoreKit Testing in Xcode(以下、Xcodeテストと呼びます)とSandboxで実験して得られた知見を、連載形式でご紹介したいと思います。本連載の実験対象と実験環境については第1回をご覧ください。
iOSアプリ内課金を実装する上で、SKPaymentTransactionObserverのpaymentQueue(_:updatedTransactions:)関数がどのタイミングで呼ばれ、引数のトランザクションの中身がどうなっているのかを正しく把握しておく必要があります。第3回となる本記事では、Interrupted PurchaseとAsk to Buyの2場面におけるpaymentQueue関数の振る舞いを紹介します。困ったことにXcodeテストとSandboxテストで挙動が微妙に違うので、それも合わせて説明していきます。
連載目次
第1回 導入編
第2回 paymentQueue関数(購入時、更新時、重複購入時)
第3回 paymentQueue関数(Interrupted Purchase、Ask to Buy) ←本記事
第4回 paymentQueue関数(解約後の再契約)
第5回 finishTransaction()を実行しないとどうなるか
↓今後公開予定↓
第6回 購入に失敗するケース
第7回 レシート
場面4. 購入処理が途中で中断されたとき(Interrupted Purchase)
購入処理を完了させるためにユーザが何らかのアクションをとる必要があるケースをInterrupted Purchaseといいます。例えば、支払情報の修正やAppleの新しい利用規約への同意が必要になる場合です。
Apple公式ドキュメントの中でInterrupted Purchaseについて触れているのはアプリ内課金のテストに関するページ(下記4ページ)だけなので、ドキュメントを隅々まで読んでいないと見落としがちかもしれません。私はWWDC2020のビデオで初めて知りました。
・Test in-app purchases - App Store Connect Help
・Testing In-App Purchases with Sandbox | Apple Developer Documentation
・Testing In-App Purchases in Xcode | Apple Developer Documentation
・Setting Up StoreKit Testing in Xcode | Apple Developer Documentation
[Xcodeテスト]
StoreKit Configuration Fileを開いた状態で Xcodeのメニューバーの Editor > Enable Interrupted Purchases をクリックし、「Disable Interrupted Purchases」に表示が切り替われば準備完了です。
アプリ内の購入ボタンを押すとtransactionStateが.purchasingのpaymentQueue(_:updatedTransactions:)が呼ばれるのは、他のケースと同じです。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:18:00]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:18:00]
課金シートで承認ボタンを押した後、paymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.failed)。error codeは0 (unknown)で、後述のSandboxテストと違い、NSUnderlyingErrorはありません。また、このときはダイアログは表示されず、UIに変化は見られません。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:18:06]
---------- transaction[0] ----------
Transaction ID = nil, State = .failed
Original Transaction ID = nil, State = nil
transaction.error = Optional(Error Domain=SKErrorDomain Code=0 "(null)")
error.domain = SKErrorDomain, error.code = 0
error.userInfo = [:]
error.localizedDescription = The operation couldn’t be completed. (SKErrorDomain error 0.)
error.localizedFailureReason = nil
error.localizedRecoveryOptions = nil
error.localizedRecoverySuggestion = nil
.failed> an unknown or unexpected error occurred.
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:18:06]
Transaction ManagerでFailedのトランザクションを選択してResolve Issuesボタンを押す(図1)と、paymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.purchased)。paymentQueue(_:updatedTransactions:)が呼ばれるタイミングは、Resolve Issues実行時にアプリがフォアグラウンドにある場合は即時、バックグラウンドにある場合はフォアグラウンドにしたタイミングです。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:20:02]
---------- transaction[0] ----------
Transaction ID = Optional("6"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:20:02]
なお、finishTransaction()をしていても、Transaction Manager上では”This transaction has not been finished by calling [SKPaymentQueue finishTransaction:]”の⚠マークが表示されます。(これはXcode12の不具合でしょうか?)
図1. Transaction ManagerでFailedのトランザクションを選択してResolve Issuesボタンを押す様子
[Sandboxテスト]
事前準備として、App Store ConnectでSandbox Apple IDを作成し、「Interrupted Purchases for This Tester」にチェックマークをつけます(図2)。そして、端末の設定アプリ > [App Store] > [SANDBOXアカウント] でそのSandbox Apple IDにサインインします。
図2. Sandbox Apple IDのInterrupted Purchasesの設定
アプリ内の購入ボタンを押すと、他のケースと同じくtransactionStateが.purchasingのpaymentQueue(_:updatedTransactions:)が呼ばれます。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [19:10:13]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [19:10:13]
図3. SandboxテストでInterrupted Purchasesを再現したときのアプリ画面
課金シートで「サブスクリプションに登録」ボタンを押した後、「Appleメディアサービスの利用規約が変更されました。続けるには利用規約をお読みの上、同意いただく必要があります [Environment: Sandbox]」というダイアログが表示されます。
そこでOKを押すと利用規約のモーダルシートが表示され、同時にpaymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.failed)。error codeは0 (unknown)で、NSUnderlyingErrorは「Error Domain=ASDServerErrorDomain Code=3038 "Apple メディアサービスの利用規約が変更されました。" 」です。
2021-02-26 19:10:54.229100+0900 SubscriptionLab[1175:776502] <SKPaymentQueue: 0x2816f0540>: Payment completed with error: Error Domain=ASDServerErrorDomain Code=3038 "Apple メディアサービスの利用規約が変更されました。" UserInfo={NSLocalizedDescription=Apple メディアサービスの利用規約が変更されました。}
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [19:10:54]
---------- transaction[0] ----------
Transaction ID = nil, State = .failed
Original Transaction ID = nil, State = nil
transaction.error = Optional(Error Domain=SKErrorDomain Code=0 "An unknown error occurred" UserInfo={NSLocalizedDescription=An unknown error occurred, NSUnderlyingError=0x281a84f90 {Error Domain=ASDServerErrorDomain Code=3038 "Apple メディアサービスの利用規約が変更されました。" UserInfo={NSLocalizedDescription=Apple メディアサービスの利用規約が変更されました。}}})
error.domain = SKErrorDomain, error.code = 0
error.userInfo = ["NSLocalizedDescription": An unknown error occurred, "NSUnderlyingError": Error Domain=ASDServerErrorDomain Code=3038 "Apple メディアサービスの利用規約が変更されました。" UserInfo={NSLocalizedDescription=Apple メディアサービスの利用規約が変更されました。}]
error.localizedDescription = An unknown error occurred
error.localizedFailureReason = nil
error.localizedRecoveryOptions = nil
error.localizedRecoverySuggestion = nil
.failed> an unknown or unexpected error occurred.
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [19:10:54]
利用規約のモーダルシートはなぜか2枚重なって表示されます(これはSandboxの不具合でしょうか?)。
1枚目のモーダルシートで「同意する」を押して3秒くらい待つとpaymentQueue(_:updatedTransactions:)が呼ばれ(transactionStateは.purchased)、「完了しました。購入手続きが完了しました。 [Environment: Sandbox]」というダイアログが表示されます。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [19:12:39]
---------- transaction[0] ----------
Transaction ID = Optional("1000000782225027"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [19:12:39]
2枚目のモーダルで「同意する」ボタンを押した後の挙動は、私が確認した限り2パターンありました。どちらになるかはタイミングによって変わるかもしれません。
パターン1:再度paymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.purchased)。Original Transaction IDは1枚目のモーダルシートで同意したときに発生したトランザクションのTransaction IDとなっています。それと同時に「現在サブスクリプションを利用中です。 [Environment: Sandbox]」というダイアログが表示されます。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [19:14:54]
---------- transaction[0] ----------
Transaction ID = Optional("1000000782226201"), State = .purchased
Original Transaction ID = Optional("1000000782225027"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [19:14:54]
パターン2:paymentQueue(_:updatedTransactions:)は呼ばれず、「完了しました。購入手続きが完了しました。 [Environment: Sandbox]」というダイアログが表示されます。
補足情報ですが、Sandbox Apple IDのInterrupted Purchasesの設定は1回購入すると解除されるため、もう一度テストしたい場合は再度App Store Connectで設定し直す必要があります。
場面5. Ask to Buyが有効になっているユーザが購入したとき
Ask to Buy(日本語版では「承認と購入のリクエスト」)はFamily Sharing(ファミリー共有)の機能のひとつで、子供がアプリ内課金をするときに親の承認を挟むことができるシステムです。詳細については次のURLをご覧ください。
・英語版 https://support.apple.com/en-us/HT201089
・日本語版 https://support.apple.com/ja-jp/HT201089
[Xcodeテスト]
StoreKit Configuration Fileを開いた状態で Xcodeのメニューバーの Editor > Enable Ask To Buy をクリックし、「Disable Ask To Buy」に表示が切り替われば準備完了です。
アプリ内の購入ボタンを押すとtransactionStateが.purchasingのpaymentQueue(_:updatedTransactions:)が呼ばれるのは、他のケースと同じです。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
図4. XcodeテストでAsk to Buyを再現したときのアプリ画面
課金シートで「承認」ボタンを押した後、「Ask Permission A request to buy "" will be sent to your parent or guardian. [Environment: Xcode]」というダイアログが表示されます。端末の言語設定が日本語であっても、Xcodeテストでは英語のダイアログが表示されます。ダイアログには「Ask」ボタンと「Cancel」ボタンが表示されていますが、どちらを選択してもpaymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.deferred)。ダイアログ表示中にホームボタンを押した場合も同じ結果になります。
コンソール上では「Payment completed with error: Error Domain=ASDErrorDomain Code=1052 "Unhandled exception" (以下略)」というエラーログが表示されますが、paymentQueueのtransaction.errorはnilです。
2021-XX-XX 00:03:23.424300+0900 SubscriptionLab[833:388044] <SKPaymentQueue: 0x2814bc830>: Payment completed with error: Error Domain=ASDErrorDomain Code=1052 "Unhandled exception" UserInfo={NSLocalizedDescription=Unhandled exception, NSLocalizedFailureReason=An unknown error occurred}
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:03:23]
---------- transaction[0] ----------
Transaction ID = nil, State = .deferred
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:03:23]
図5. Transaction Managerで承認または否認する様子
図5のようにTransaction ManagerでPending Approvalのトランザクションを選択してApprove Transactionsボタンを押すと承認され、paymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.purchased)。paymentQueue(_:updatedTransactions:)が呼ばれるタイミングは、承認時にアプリがフォアグラウンドにある場合は即時、バックグラウンドにある場合はフォアグラウンドにしたタイミングです。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:04:36]
---------- transaction[0] ----------
Transaction ID = Optional(""8""), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:04:36]
Transaction ManagerでDecline Transactionsボタンを押して否認した場合は何も起こりません。paymentQueue(_:updatedTransactions:)も呼ばれません。
[Sandboxテスト]
Sandboxテストでは、Ask to Buyの一連の処理のうち一部しかテストできません。Testing at All Stages of Development with Xcode and Sandboxに記載されている通り、state=deferredのトランザクションは起動できますが、そのトランザクションに対する承認や否認はできません。
また、テスト用にソースコードを次のように書き換える必要があります。コードの書き方についてはWWDC2016のビデオ「Using Store Kit for In-App Purchases with Swift 3」の19分ごろで解説されています。mutable paymentを作成し、simulatesAskToBuyInSandboxフラグをtrueに設定して購入フローを起動します。このやり方がTesting In-App Purchases with Sandboxのページに載っておらず、不親切だなと感じました。
let payment = SKMutablePayment(product: product)
payment.simulatesAskToBuyInSandbox = true
SKPaymentQueue.defaultQueue().add(payment)
アプリ内の購入ボタンを押すとtransactionStateが.purchasingのpaymentQueue(_:updatedTransactions:)が呼ばれるのは、他のケースと同じです。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
図6. SandboxテストでAsk to Buyを再現したときのアプリ画面
課金シートで「サブスクリプションに登録」ボタンを押した後、3秒くらい待つと「承認をリクエスト 『●●』のサブスクリプションリクエストが保護者の方へ送信されます。 [Environment: Sandbox]」というダイアログが表示されます。
ダイアログには「承認を求める」ボタンと「キャンセル」ボタンが表示されていますが、どちらを選択しても3秒後くらいにpaymentQueue(_:updatedTransactions:)が呼ばれます(transactionStateは.deferred)。ダイアログ表示中にホームボタンを押した場合も同じ結果になります。
コンソール上では「Payment completed with error: Error Domain=ASDErrorDomain Code=1052 "Unhandled exception" (以下略)」というエラーログが表示されますが、paymentQueueのtransaction.errorはnilです。
2021-XX-XX 00:01:09.012344+0900 SubscriptionLab[821:350096] <SKPaymentQueue: 0x280380610>: Payment completed with error: Error Domain=ASDErrorDomain Code=1052 "Unhandled exception" UserInfo={NSUnderlyingError=0x280ff04b0 {Error Domain=AMSErrorDomain Code=511 "Family permission required" UserInfo={NSLocalizedDescription=Family permission required}}, NSLocalizedFailureReason=An unknown error occurred, NSLocalizedDescription=Unhandled exception}
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:09]
---------- transaction[0] ----------
Transaction ID = nil, State = .deferred
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:09]
前述の通りSandboxテストでは承認と否認はテストできません。
宣伝
NTTレゾナントテクノロジーでは一緒に働いてくれるAndroid/iOSアプリエンジニアを募集中です。もし興味がありましたら採用ページをご覧ください。