StoreKit トランザクションの重複を防ぐ


トランザクションが複数できると面倒なことになると前回のポストで書きました。特に購入手続きの途中でアプリケーションが終了してしまったような場合は、アプリケーション側で少し面倒ですが対策をいれないと問題を回避できません。

発生するケースについての確認

実際の対策を見る前に、発生するケースについて確認しておきます。

  1. ユーザがアプリケーション上で特定のアイテムを「購入」しようとする。
  2. アプリケーションからStoreKitのキューにPaymentリクエストが積まれる。
  3. トランザクションが生成される。
  4. トランザクションが完了する前にアプリケーションが終了。
  5. アプリケーション再起動。
  6. 前回のトランザクションが再開/完了するより前のタイミングでユーザが再び「購入」しようとする。

この問題は、前述の通りアプリケーション起動から未完状態のトランザクションが再開されるまで若干のラグがあるために発生しうる問題です。

対策の方針

この問題は、未完トランザクションが残っている場合はユーザに6.を実行させない、「購入ボタン」等を無効にしておくことで回避できます。ただし、前述の通りアプリケーション側から「未完状態のトランザクションが残っているか」「再開されたのかどうか」をStoreKitに問い合わせる事ができません。

未完トランザクションをカウントしておく

苦肉の策ですが、この問題を解決するにはトランザクションの数をアプリケーション側で覚えておくしかないと思います。購入手続きを開始するタイミングでカウントを1増やし、トランザクションがfinishするごとに1減らしていく形で。そうすれば、このカウントをみることで未完状態のトランザクションがあることがわかるので、その状態でのユーザの購入を防ぐ事ができます。

カウントアップ

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchasing:
                // Count up
                @synchronized(self) {
                    NSInteger numOfTransaction = [[NSUserDefaults standardUserDefaults] integerForKey:@"NumberOfTransactions"];
                    [[NSUserDefaults standardUserDefaults] setInteger:numOfTransaction+1 forKey:@"NumberOfTransactions"];
                    [[NSUserDefaults standardUserDefaults] synchronize];
                }
                break;

キューに積むタイミングでも良いのですが、キューに積んだトランザクションの処理が開始されたタイミング(ステートがSKPaymentTransactionStatePurchasingになったタイミング)の方が適切そうなので、そっちでカウントアップしています。

カウントダウン

finishTransaction:のタイミングでも良いのですが、成功した場合/失敗した場合の両方に書くのは少し冗長なので、トランザクションが完了した際に呼ばれるコールバックpaymentQueue:removedTransactions:を追加して、そこでカウントダウンしたほうが楽です。

- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        // Count down
        @synchronized(self) {
            NSInteger numOfTransaction = [[NSUserDefaults standardUserDefaults] integerForKey:@"NumberOfTransactions"];
            [[NSUserDefaults standardUserDefaults] setInteger:numOfTransaction-1 forKey:@"NumberOfTransactions"];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
    }
}

購入可否の判定

あとは、NSUserDefaultsのNumberOfTransactionsの値を見るだけで二重購入を防ぐ事ができるようになります。

「キャンセル」された場合は?

ちなみに、もしアプリケーションが終了した後で購入確認ダイアログがでて、そこでユーザが「キャンセル」を選択した場合、次回起動時にそのトランザクションがfailしたことが通知されるので、この場合も問題ありません。(iOS 5で確認)

問題点

とはいえ、残念ながらこの対策は完全ではありません。

前述の通り、複数端末で同じApple IDを使用している場合や、AppStoreからサインアウトしてしまった場合、トランザクションの再開時には認証ダイアログが表示されます。ここで正しく認証された場合は問題ないのですが、キャンセルが押された場合、トランザクションは再開されず、updatedTransactionsもremovedTransactionsも呼ばれません。

このトランザクションが完了しない限り、NumberOfTransactionsの値は0にならないので、ユーザはプロダクトを購入できなくなってしまいます。そして、アプリ側からトランザクションに紐づくApple IDの情報を取得することはできないため、この問題はタイムアウトを入れる(起動してから数分たっても状態がかわらなかったら、デクリメントする)等無理矢理な方法で解決するしかなさそうです。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です