iOSアプリ内課金をテストし尽くす〜第5回 finishTransactionを実行しないとどうなるか〜[Xcode12/iOS14]
NTTレゾナントテクノロジー アジャイルデザイン部の西添です。
この記事は連載「iOSアプリ内課金をテストし尽くす」の第5回目です。この連載では、iOSアプリ内課金(In-App Purchase)のサブスクリプションを題材に、様々な場面でStoreKitフレームワークがどのように振る舞うのかを実験結果をもとに紹介しています。実験はStoreKit Testing in Xcode(以下、Xcodeテストと呼びます)とSandboxの2つの方法で実施しています。実験対象と実験環境の詳細については第1回をご覧ください。
iOSアプリ内課金を実装する上で、SKPaymentTransactionObserverのpaymentQueue(_:updatedTransactions:)関数がどのタイミングで呼ばれ、引数のトランザクションの中身がどうなっているのかを正しく把握しておく必要があります。第5回となる本記事では、paymentQueue(_:updatedTransactions:)関数の引数updatedTransactionsに入っている各トランザクションに対してfinishTransaction(_:)関数を実行しない場合、その後の挙動がどうなるかについてを紹介します。なお、本記事では第1回で掲載したソースコードにあるfinishTransaction()を適宜コメントアウトして動作確認しています。
連載目次
第1回 導入編
第2回 paymentQueue関数(購入時、更新時、重複購入時)
第3回 paymentQueue関数(Interrupted Purchase、Ask to Buy)
第4回 paymentQueue関数(解約後の再契約)
第5回 finishTransaction()を実行しないとどうなるか ←本記事
↓今後公開予定↓
第6回 購入に失敗するケース
第7回 レシート
A. purchasingのトランザクション
.purchasingのトランザクションに限ってはfinishTransaction()を実行しないのが正解です。むしろ.purchasingのトランザクションに対してfinishTransaction()を実行してしまうと、以下のような例外が発生します。XcodeテストもSandboxテストも同じログが出力されます。
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:24:19]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
2021-09-15 17:24:19.366272+0900 SubscriptionLab[543:17883] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot finish a purchasing transaction'
*** First throw call stack:
(0x1a814a5ac 0x1bc1c442c 0x1a8046a9c 0x1c4ea59b8 0x102293274 0x102294974 0x102667b68 0x1026695f0 0x102678890 0x1a80c71e4 0x1a80c13b4 0x1a80c04bc 0x1beb45820 0x1aaa64734 0x1aaa69e10 0x1bb4fc6ec 0x102296870 0x1022967e8 0x1022968b4 0x1a7d87e60)
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot finish a purchasing transaction'
terminating with uncaught exception of type NSException
B. purchasedのトランザクション
B-1. サブスクリプションを購入したとき
サブスクリプションを購入したときに.purchasedトランザクションに対してfinishTransaction()を実行しない場合、次回のアプリ起動時にpaymentQueue関数が呼ばれます。アプリをバックグラウンドからフォアグラウンドにしたときには何も起きません。
[Xcodeテスト]
▼サブスクリプション開始(finishTransactionは実行しない)
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:46:30]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:46:30]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:46:50]
---------- transaction[0] ----------
Transaction ID = Optional("1"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:46:50]
▼アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:48:01]
---------- transaction[0] ----------
Transaction ID = Optional("1"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:48:01]
[Sandboxテスト]
▼サブスクリプション開始(finishTransactionは実行しない)
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:29:31]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:29:31]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:30:00]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870088177"), State = .purchased
Original Transaction ID = Optional("1000000859407587"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:30:00]
▼アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:31:02]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870088177"), State = .purchased
Original Transaction ID = Optional("1000000859407587"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:31:02]
B-2. サブスクリプションが更新されたら、finishTransactionしなかったトランザクションはどうなるのか?
.purchasedのトランザクションに対してfinishTransaction()を実行しないままサブスクリプションが更新されて次の契約期間を迎えた場合は、次回のアプリ起動時にpaymentQueue関数が呼ばれます。アプリをバックグラウンドからフォアグラウンドにしたときには何も起きません。
[Xcodeテスト]
▼サブスクリプション開始(finishTransactionは実行しない)
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:41:50]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:41:50]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:42:03]
---------- transaction[0] ----------
Transaction ID = Optional("7"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:42:03]
▼バックグラウンド状態でサブスクリプションが3回更新されるまで待機してからフォアグラウンド復帰
(finishTransactionは実行しない)
※これは第2回で紹介した挙動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:43:35]
---------- transaction[0] ----------
Transaction ID = Optional("8"), State = .purchased
Original Transaction ID = Optional("7"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:43:35]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:43:35]
---------- transaction[0] ----------
Transaction ID = Optional("9"), State = .purchased
Original Transaction ID = Optional("7"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:43:35]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:43:35]
---------- transaction[0] ----------
Transaction ID = Optional("10"), State = .purchased
Original Transaction ID = Optional("7"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:43:35]
▼アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:43:44]
---------- transaction[0] ----------
Transaction ID = Optional("9"), State = .purchased
Original Transaction ID = Optional("7"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("10"), State = .purchased
Original Transaction ID = Optional("7"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
Transaction ID = Optional("8"), State = .purchased
Original Transaction ID = Optional("7"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[3] ----------
Transaction ID = Optional("7"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:43:44]
[Sandboxテスト]
▼サブスクリプション開始(finishTransactionは実行しない)
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:50:39]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:50:39]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:51:10]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870112187"), State = .purchased
Original Transaction ID = Optional("1000000858789433"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:51:10]
▼バックグラウンド状態でサブスクリプションが2回更新されるまで待機してからフォアグラウンド復帰
(finishTransactionは実行しない)
※これは第2回で紹介した挙動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:02:31]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870117694"), State = .purchased
Original Transaction ID = Optional("1000000858789433"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000870124455"), State = .purchased
Original Transaction ID = Optional("1000000858789433"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:02:31]
▼アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:03:23]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870112187"), State = .purchased
Original Transaction ID = Optional("1000000858789433"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000870117694"), State = .purchased
Original Transaction ID = Optional("1000000858789433"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
Transaction ID = Optional("1000000870124455"), State = .purchased
Original Transaction ID = Optional("1000000858789433"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:03:23]
上記のログから、次回アプリ起動時にpaymentQueue関数が呼ばれ、paymentQueue関数の引数「updatedTransactions」にはfinishTransaction()を実行していない全てのトランザクション(初回分+更新分)が入っていることがわかります。
B-3. サブスクリプションを解約したら、finishTransactionしなかったトランザクションはどうなるのか?
サブスクリプションの有効期限や解約状況に関わらず、finishTransactionを実行していない.purchasedトランザクションが存在する場合は、次回のアプリ起動時にpaymentQueue関数が呼ばれます。アプリをバックグラウンドからフォアグラウンドにしたときには何も起きません。
[Xcodeテスト]
▼サブスクリプション開始(finishTransactionは実行しない)
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:59:30]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:59:30]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:59:50]
---------- transaction[0] ----------
Transaction ID = Optional("3"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:59:50]
▼Transaction Managerでcancel subscriptionを実行後、アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [19:00:33]
---------- transaction[0] ----------
Transaction ID = Optional("3"), State = .purchased
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [19:00:33]
[Sandboxテスト]
▼サブスクリプション開始(finishTransactionは実行しない)
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:25:19]
---------- transaction[0] ----------
Transaction ID = nil, State = .purchasing
Original Transaction ID = nil, State = nil
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:25:19]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:26:10]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870150255"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:26:10]
※2回目の更新の後にサブスクリプションをキャンセル
▼キャンセル後、有効期限内にアプリ起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:38:53]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870150255"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000870154286"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
Transaction ID = Optional("1000000870159127"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:38:53]
▼キャンセル後、有効期限切れ後にアプリ起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [18:45:57]
---------- transaction[0] ----------
Transaction ID = Optional("1000000870150255"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000870154286"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
Transaction ID = Optional("1000000870159127"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [18:45:57]
C. failedのトランザクション
.failedのトランザクションについてはfinishTransaction()を実行しても実行しなくても挙動は変わりません。どちらの場合でも、フォアグラウンド復帰時やアプリ起動時にpaymentQueue関数は呼ばれません。
動作確認方法:
アプリで購入ボタンを押した後に表示されるAppStoreのモーダルシートでキャンセルを押すと.failedのトランザクションが発生します。その後、(1)アプリをバックグラウンドにしてからフォアグラウンドに戻したり、(2)アプリを終了させてから起動し直したりしても、paymentQueue関数は呼ばれませんでした。
D. restoredのトランザクション
.restoredのトランザクションは SKPaymentQueue.default().restoreCompletedTransactions() を実行すると発生します。
.restoredのトランザクションに対してfinishTransaction()を実行しない場合、(1)アプリをバックグラウンドからフォアグラウンドにした時や(2)アプリを再起動した時にpaymentQueue関数が呼ばれます。その際、transactionStateは.restoredではなく.purchasedになります。
逆に.restoredのトランザクションに対してfinishTransaction()を実行する場合は、フォアグラウンド復帰時やアプリ起動時にpaymentQueue関数は呼ばれません。
[Xcodeテスト]
▼restoreCompletedTransactions()の実行
restoreCompletedTransactions() >>>>>>>>>> [17:49:48]
restoreCompletedTransactions() <<<<<<<<<< [17:49:48]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:49:48]
---------- transaction[0] ----------
Transaction ID = Optional("4"), State = .restored
Original Transaction ID = Optional("0"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:49:48]
▼(1)フォアグラウンド復帰
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:50:14]
---------- transaction[0] ----------
Transaction ID = Optional("4"), State = .purchased
Original Transaction ID = Optional("0"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:50:14]
▼(2)アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:51:01]
---------- transaction[0] ----------
Transaction ID = Optional("4"), State = .purchased
Original Transaction ID = Optional("0"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:51:01]
[Sandboxテスト]
▼restoreCompletedTransactions()の実行
restoreCompletedTransactions() >>>>>>>>>> [17:39:22]
restoreCompletedTransactions() <<<<<<<<<< [17:39:22]
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:39:23]
---------- transaction[0] ----------
Transaction ID = Optional("1000000878021860"), State = .restored
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000878021861"), State = .restored
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
(省略)
---------- transaction[8] ----------
Transaction ID = Optional("1000000878021868"), State = .restored
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:39:23]
▼(1)フォアグラウンド復帰
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:39:56]
---------- transaction[0] ----------
Transaction ID = Optional("1000000878021860"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000878021861"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
(省略)
---------- transaction[8] ----------
Transaction ID = Optional("1000000878021868"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:39:56]
▼(2)アプリ再起動
paymentQueue(_:updatedTransactions:) >>>>>>>>>> [17:40:44]
---------- transaction[0] ----------
Transaction ID = Optional("1000000878021860"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[1] ----------
Transaction ID = Optional("1000000878021861"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
---------- transaction[2] ----------
(省略)
---------- transaction[8] ----------
Transaction ID = Optional("1000000878021868"), State = .purchased
Original Transaction ID = Optional("1000000818256277"), State = Optional(".purchased")
transaction.error = nil
paymentQueue(_:updatedTransactions:) <<<<<<<<<< [17:40:44]
E. deferredのトランザクション
.deferredのトランザクションはAsk to Buyが有効になっているユーザが購入したときに発生します(参考:第3回)。
.deferredのトランザクションについてはfinishTransaction()を実行しても実行しなくても挙動は変わりません。どちらの場合でも、フォアグラウンド復帰時やアプリ起動時にpaymentQueue関数は呼ばれません。
動作確認方法:
第3回で紹介した方法でAsk to Buyの購入をした後、(1)アプリをバックグラウンドにしてからフォアグラウンドに戻したり、(2)アプリを終了させてから起動し直したりしても、paymentQueue関数は呼ばれませんでした。
宣伝
NTTレゾナントテクノロジーでは一緒に働いてくれるAndroid/iOSアプリエンジニアを募集中です。もし興味がありましたら採用ページをご覧ください。