新しい定期購読型のアプリ内課金を実装してみた (Auto-renewable Subscriptions) このエントリをはてなブックマークに登録

2011年03月11日

amachinamachin / ,


[追記1]
サンドボックス環境では自動継続がある条件下でオフになることがあるそうです。
この記事の情報は不正確な可能性があるため、近日中に検証します。

[追記2]
調査したところ、iPadで2ヶ月購読した場合は動作することは確認できました。
ただ現状ではアプリに組み込んでリリースするのは危険だとは思います。

参考

Auto-Renewable Subscrptionに関する問題など

注意点

まだ新しい定期購読型のアプリ内課金を使ったアプリはリリースしていません。
不明な点があるため間違いなどありましたら、ぜひご連絡ください。
特に自動更新後、アプリ側から有効期限を確認する方法が正しいかどうかが不明です。

Twitterfacebook

新しい定期購読型のアプリ内課金は何が違うの?

新しい定期購読型のアプリ内課金は、Appleが2011/2/15に発表した自動更新型の課金サービスです。
今までもアプリ内課金では購読型(Subscription)があったのですが、定期購読の期限の管理や更新時の請求は開発者任せなため、外部サーバを必要としました。
今回追加された自動更新(Auto-Renewable)は自動更新で課金されるため、購読情報をApple側に任せることが出来ます。

購読期間

  • 1週間
  • 1ヶ月
  • 2ヶ月
  • 3ヶ月
  • 6ヶ月
  • 1年

クレイではアプリ内課金と連携したコンテンツ管理システムを持っているため、今回の新しい課金サービスにも対応出来るように調査、実装してみました。

実装の流れと注意点

ドキュメントを読むと、既存の非消耗型(Non-consumables)と同じクライアントのコードがそのまま使えると書いてあります。確かにそのままProduct IDを変更するだけで決済自体は出来るのですが、次の点に注意する必要があると思います。

アプリから定期的に有効期限を確認する
自動更新を途中で止めた場合にアプリから確認する方法が必要。

アプリから購読情報を復元する
アプリを削除して再インストールした後など購読情報を取得する方法が必要。

上記の点を考慮して、次のような流れで組んでいきます。

  1. itunes connectの準備
  2. 非消耗型と同じ購入を実装
  3. アプリから有効期限を確認
  4. 定期的に有効期限を確認
  5. アプリから購読情報を復元

itunes connectの準備

itunes connectに自動更新の商品を追加する必要があるのですが、その前にやることが増えています。

Shared Secretというユニークなコードを作成する必要があります。
これを取得してレシートを確認する時に一緒に送ることで購読情報を取得することが出来るようになります。

作成するためには、メニューからManage Your In App Purchasesを選択して、Shared Secretの Generate Shared Secret ボタンを押してください。
Shared Secretの項目が無い場合は、Contracts, Tax, and Bankingの新しい規約を承認してください。


非消耗型と同じ購入を実装

わかりやすいようにまず非消耗型の決済の実装を行います。
以前の記事で実装について詳しく書きましたので、そちらを参考にしてください。

参考

アプリから有効期限を確認

有効期限を確認するためにはレシートをAppleのサーバに投げることで行います。
Appleのサーバは本番環境とSandbox環境で違うので注意してください。

  • https://sandbox.itunes.apple.com/verifyReceipt
  • https://buy.itunes.apple.com/verifyReceipt
- (NSDictionary *)verifyReceipt:(NSString *)base64Receipt {
#ifdef DEBUG
    NSString *urlsting = @"https://sandbox.itunes.apple.com/verifyReceipt";
#else
    NSString *urlsting = @"https://buy.itunes.apple.com/verifyReceipt";
#endif
   
    NSURL *url = [NSURL URLWithString:urlsting];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    NSString *json = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\", \"password\":\"%@\"}", base64Receipt, SHARED_SECRET];
    [request setHTTPBody:[json dataUsingEncoding:NSUTF8StringEncoding]];
    [request setHTTPMethod:@"POST"];
   
    NSError *error;
    NSURLResponse *response;
    NSData *decodeData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];    
    NSString *receipt = [[NSString alloc] initWithData:decodeData encoding:NSUTF8StringEncoding];
   
    NSDictionary *dict = [receipt JSONValue];
    [receipt release];
   
    return dict;
}

- (NSInteger)savePurchaseInfo:(NSDictionary *)dict {
    NSNumber *status = [dict objectForKey:@"status"];
   
    if ([status isEqual:[NSNumber numberWithInt:0]] == NO) {
        LOG(@"購入データの検証に失敗: %@", status);
        return [status integerValue];
    }
   
    NSDictionary *receiptDict = [dict objectForKey:@"receipt"];
    NSNumber *expired = [receiptDict objectForKey:@"expires_date"];
    NSString *latestReceipt = [dict objectForKey:@"latest_receipt"];
   
    // クライアントに保存する期間を更新
    TRConfig *config = [TRConfig instance];
    if (config.subscriptionExpired == nil || [config.subscriptionExpired compare:expired] == NSOrderedAscending) {
        [config setSubscriptionExpired:expired];
        [config setSubscriptionLatestReceipt:latestReceipt];
    }
    return 0;
}


- (void)decodeReceiptWithTransaction:(SKPaymentTransaction *)transaction {
    NSDictionary *dict = [self verifyReceipt:[Base64 encode:transaction.transactionReceipt]];

    NSInteger code = [self savePurchaseInfo:dict];
    if (code != 0) {
        // 21006は期限切れの場合のレシートの時のエラーコード。リストア処理の場合に発生するためアラート処理は除外する。
        if (code != 21006) {
            NSString *message = [NSString stringWithFormat:@"購入データの検証に失敗しました (%d)", code];
            KKALERT(LOCALIZE(@"Purchase Error"), message);
        }
        return;
    }
}


expires_datelatest_receiptをアプリに保存して確認用に利用するのが良さそうです。
TRConfigは設定を保存するクラス、KKALERTはアラートを表示するマクロです。

定期的に有効期限を確認

起動時やバックグラウンドから復帰した場合に、有効期限を確認するようにします。
TRConfigやTRStorekitといったクラスが書かれていますが、アプリに保存したlatest_receiptをAppleのサーバに投げて確認しています。

- (void)updateSubscriptionExpired {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString *latestReceipt = [[TRConfig instance] subscriptionLatestReceipt];
    NSDictionary *receiptDict = [TRStoreKit verifyReceipt:latestReceipt];
    [TRStoreKit savePurchaseInfo:receiptDict];
    [pool release];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
#if TARGET_OS_IPHONE
    [self performSelectorInBackground:@selector(updateSubscriptionExpired) withObject:nil];
#endif
}

アプリから購読情報を復元

今回実装してみてわかったことなのですが、アプリを一度削除してから再インストールした後で、購入ボタンから購入するフローでは購読情報を取得できないようです。というのは購入済みの状態で購入ボタンを押すと最初に「購入しますか?」のアラートが出て、OKを押すと「購読済み」のアラートが出ます。そのままOKを押すと購入自体はキャンセル扱いになってしまうようです。
(非消耗型の場合、一度購入したアイテムを再度購入しようとすると、「このアイテムはすでに購入されています。もう一度無料でダウンロードするにはOKをタップします。」と出て、OKを押すとそのまま購入になります。)

わかりにくいので、実際のコードで説明しますと、下記の流れの中で、SKPaymentTransactionStateFailedが返ってきてしまって購入処理が終わってしまいます。そのため、復元は別のボタンで実装した方が良さそうだと判断しました。

    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
                // 購入中
            case SKPaymentTransactionStatePurchasing: {
                LOG(@"Payment Transaction Purchasing");
                break;
            }
                // 購入成功
            case SKPaymentTransactionStatePurchased: {
                LOG(@"Payment Transaction END Purchased: %@", transaction.transactionIdentifier);
                [self completeTransaction:transaction];
                break;
            }
                // 購入失敗
            case SKPaymentTransactionStateFailed: {
                LOG(@"Payment Transaction END Failed: %@ %@", transaction.transactionIdentifier, transaction.error);
                [self failedTransaction:transaction];
                break;
            }
                // 購入履歴復元
            case SKPaymentTransactionStateRestored: {
                LOG(@"Payment Transaction END Restored: %@", transaction.transactionIdentifier);
                [self restoreTransaction:transaction];
                break;
            }
        }
    }
- (void)restore {
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}


#pragma mark -
#pragma mark restore transaction

- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
    for (SKPaymentTransaction *transaction in queue.transactions) {
        LOG(@"Restore: %@", transaction.transactionIdentifier);
    }
    LOG(@"Payment Restore Transaction Finished");
   
    if ([config isSubscription] == NO) {
        KKALERT(nil, LOCALIZE(@"Restore Failed"));
    }
}

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
    for (SKPaymentTransaction *transaction in queue.transactions) {
        LOG(@"Restore: %@", transaction.transactionIdentifier);
    }
    LOG(@"Payment Restore Transaction Error: %@", [error localizedDescription]);
   
    if ([[TRConfig instance] isSubscription] == NO) {
        KKALERT(nil, LOCALIZE(@"Restore Failed"));
    }
}
(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
(void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error


restoreCompletedTransactions が呼ばれた後、updatedTransactions が呼ばれます。
その後、paymentQueueRestoreCompletedTransactionsFinished か、restoreCompletedTransactionsFailedWithError が呼ばれます。

ただし、paymentQueueRestoreCompletedTransactionsFinished が呼ばれたこと = 購読処理がうまくいったわけではないので、注意してください。

まとめ

今回も長くなってしまいました。。
コードが一つの流れで説明出来なかったため、わかりにくいとは思います。
正直私も手探りの部分があるため、もっと良い方法があると思っています。

ということで、もっと良い方法をみつけた人はぜひ教えてください。

さて、そろそろ本業のウェブシステムの世界に戻ります。


クレイについてもっと知りたい方は…

  1. クレイの3つの強みを見てみる。
  2. WEBシステムのことなら何でもご相談ください。

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

  1. レシートの確認をする際のパスワードなんですが
    “SHARED_SECRET”を指定していますがこれには何が入っているのでしょうか?

    G-sTyLe

    2012年08月31日, 4:47 PM

  2. itunes connectで取得したコードを入れています。
    「itunes connectの準備」の項目に書かれていますので、ご確認ください。

    amachin

    2012年09月14日, 11:55 AM

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ