StoreKit APIを使う上で不便な点の一つに、生成したTransactionを追跡するのが難しい、という問題があります。PaymentをQueueに積むとTransactionが生成されますが、その生成されたTransactionの情報を取得する方法について調べていきましょう。
Paymentの追加
アプリ内課金の手続きは、SKPaymentQueueにPaymentを追加するaddPayment:を呼ぶ事でスタートします。
SKPayment* payment = [SKPayment paymentWithProduct:someProduct]; [[SKPaymentQueue defaultQueue] addPayment:payment];
QueueにPaymentが追加されると、Transactionが作成/開始されます。
When a payment request is added to the queue, the payment queue processes that request with the Apple App Store and arranges for payment from the user. When that transaction is complete or if a failure occurs, the payment queue sends the SKPaymentTransaction object that encapsulates the request to all transaction observers.
ドキュメントには上記のような説明がありますが、PaymentからどのタイミングでTransactionが生成されるのか、については厳密な説明がありません。同期的に行われるのか、非同期で行われるのか、詳細は不明です。
- (void)addPayment:(SKPayment *)payment
同期的に処理されるのであればaddPayment:メソッドが返り値で生成したSKPaymentTransactionオブジェクトを返してくれればハッピーなのですが、addPayment:メソッドの返り値はvoidです。
Asynchronous. Add a payment to the server queue. The payment is copied to add an SKPaymentTransaction to the transactions array. The same payment can be added multiple times to create multiple transactions.
また、ドキュメントにはありませんが、StoreKit/SKPaymentQueue.hには上記のように「非同期」であることを示すコメントが存在します。例によって処理のどの部分からが非同期なのか、は説明がないのですが、ひとまず非同期処理が含まれることだけ頭に入れておきましょう。
生成されたトランザクションを取得する
アプリケーション側からStoreKitに決済開始時に指定する内容はPayment、StoreKitからアプリケーション側にデリゲートで通知されるときのオブジェクトはTransaction。セキュアなコードを書くには、PaymentとTransactionの関係を把握する必要があるので、PaymentとTransactionの関係をどうやったら把握できるか、考えてみましょう。
1. SKPaymentTransactionオブジェクトのpaymentオブジェクトで判断する
SKPaymentTransactionオブジェクトにはpaymentプロパティが存在します。クラスリファレンスにおけるSKPaymentTransactionのpaymentプロパティに関する説明にも、
Each payment transaction is created in response to a payment that your application added to the payment queue.
という記述があるので、アプリケーション側からaddPayment:で指定したPaymentを保存しておき、paymentQueue:updatedTransactions:で通知があったTransactionのpaymentプロパティと比較する、というアイデアが考えられます。
しかし、残念ながらこのアイデアはうまく行きません。addPayment:で指定したオブジェクトとSKPaymentTransactionのpaymentプロパティに保持されるオブジェクトは別のオブジェクトになっているようです。実際、StoreKit/SKPaymentQueue.hのaddPayment:に関するコメントにも
The payment is copied to add an SKPaymentTransaction to the transactions array.
という記載があり、SKPaymentTransactionのpaymentプロパティに保持されているオブジェクトはコピーされたオブジェクトになっているようです。
では、Paymentオブジェクトの中身で比較するのはどうか?これも良いアイデアとは言えません。同じプロダクトを別のタイミングで再度購入しようと試みた場合には、同じPaymentと判定されてしまうためです。
2. SKPaymentQueueのtransactionsの変化で判断する
SKPaymentQueueにはpending状態のTransactionのリストを返すtransactionsプロパティが存在します。addPayment:をする前と後で、このtransactionsプロパティの変化を見れば、addPayment:によって追加されたTransactionオブジェクトを取得できるのではないか?
このアイデアは悪くないように見えます。実際、下記のようなコードを書いてみると、transactionsの要素の数が変化していて、追加されたTransactionを取得できます。
NSLog(@"Transactions before: %@", [SKPaymentQueue defaultQueue].transactions); [[SKPaymentQueue defaultQueue] addPayment:payment]; NSLog(@"Transactions after: %@", [SKPaymentQueue defaultQueue].transactions);
実行結果
21:42:22.368 StoreKitTest[4566:707] Transactions before: ( ) 21:42:22.382 StoreKitTest[4566:707] Transactions after: ( "<SKPaymentTransaction: 0x3359f0>" )
ただし、この方法には注意が必要です。まず第一に、どこにもaddPayment:メソッドが処理を返す前にQueueのtransactionsへTransactionが追加されていることへの説明/保証がないことです。transactionsへの追加は呼び出しスレッド以外のところで非同期で行われている可能性もあり、もしそうであればaddPayment:が処理を返した直後にはtransactionsへTransactionが追加されていない可能性は十分考えられます。
また、transactionsプロパティの説明
Returns an array of pending transactions. (read-only)
pending状態のTransactionというのがどういったものを指すのか、についても詳しい説明がありません。ちなみに、ヘッダファイルでは
Array of unfinished SKPaymentTransactions.
という説明になっています。
この方法は一応動いているようには見えますが、不安は残ります。
3. paymentQueue:updatedTransactions:のイベントで判断する
addPayment:でPaymentを追加して、Transactionが生成されるとpaymentQueue:updatedTransactions:が呼ばれます。三つ目の方法はこのデリゲートイベントを利用した判定です。
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing: NSLog(@"SKPaymentTransactionStatePurchasing: %@", transaction); break; default: break; } } }
先ほどのコードに加えて、ObserverのpaymentQueue:updatedTransactions:でSKPaymentTransactionStatePurchasingのTransactionが通知された時にログを吐き出してみると、下記の用な結果が得られます。
21:02:51.631 StoreKitTest[4740:707] Transactions before: ( ) 21:02:51.646 StoreKitTest[4740:707] SKPaymentTransactionStatePurchasing: <SKPaymentTransaction: 0x1626b0> 21:02:51.648 StoreKitTest[4740:707] Transactions after: ( "<SKPaymentTransaction: 0x1626b0>" )
Queueのtransactionsを見るやり方で取得できるものと同じインスタンスを取得できています。
結論からいうと、transactionsを見るやり方よりはこちらの方が安全だと考えられます。理由はデバッガでスタックを表示すればわかります。
addPayment:からpaymentQueue:updatedTransactions:までは、同期的に同じスレッド上で処理されていることがわかります。一方でtransactionsの変更はどのタイミングで行われているか、調べるのが困難です。
このイベントを使う方法についても、ドキュメントでは説明されていない方法で将来的にaddPayment:の挙動が変わる可能性もあるので確実とは言えませんが、現状ではtransactionsの変更を見て判定するよりは若干こちらのほうが安心だと言えるでしょう。
結論
結論としては、不確かな部分が多くてaddPayment:を呼んだ直後にpaymentQueue:updatedTransactions:で通知されたSKPaymentTransactionStatePurchasing状態のTransactionが、addPayment:によって生成されたTransactionであると考えるのが良さそうだ、という程度のことしか言えません。なので、いっそPaymentとTransactionの関係を把握することについては諦めてしまう、というのも一つの選択肢です。ただ、その場合中断していた別のトランザクションが再開した場合等に、UIの制御やゲーム側での制御が厄介になることもあるので、悩ましいところです。
同期処理で動いていることを考えると、addPayment:がTransactionを返すようにすることは技術的に難しくないはずなので、将来のアップデートでそのように変更になることを願いましょう。