月額課金モデル(Auto-Renewable Subscriptions)
iOS(iPhone、iPad、iPod touch)で課金機能を実装したアプリを開発する場合は、Appleが提供している下記の課金モデルの中から選んで利用します。
・Consumable
・Non-Consumable
・Auto-Renewable Subscriptions (本ページで扱う課金モデルです)
・Free Subscription
・Non-Renewing Subscription
ページでは、今後利用が広がると予想されるAuto-Renewable Subscriptionsについての記事です。
特徴
Auto-Renewable Subscriptionsの特徴は以下のとおりです。
・7日
・1ヶ月
・2ヶ月
・3ヶ月
・6ヶ月
・1年
活用例
・電子書籍アプリや雑誌アプリの定期購読処理
・プレミアムサービス(ただし、リジェクトされる可能性が高い)
注意点
・利用可能なアプリについて
Auto-Renewable Subscriptionsは雑誌アプリや新聞アプリの定期購読で利用されることを想定しているため、フリーミアムモデルのプレミアム会員等で利用した場合、アップルの審査でリジェクト(差し戻し)される可能性が高いようです。 当社のヘアカタログアプリ(STA-LOG)のプレミアム会員機能についてもAuto-Renewable Subscriptionsで実装してAppStoreへ公開しようとしたのですが、上記理由によりリジェクトされたため、最終的にはConsumable(消費型)で実装し、サーバ側で課金状態を保有することにしました。 しかしながら、クックパッドのプレミアム会員やナビタイムのプレミアムコースで利用されていることが確認できるため、審査の基準はまちまちのようです。
・テスト環境(サンドボックス)について
アプリを開発してAuto-Renewable Subscriptionsのテストを実施する場合、実際の課金期間(1年など)では長すぎるため、短い期間でテストできるようにアップルが課金用のテスト環境(サンドボックス)を用意してくれています。
(参照:http://d.hatena.ne.jp/iRSS/20111028/1319763704) しかしながらこのテスト環境の仕様が非常に不明瞭で、予期しない動作をすることが多く、テストが非常に大変です。詳細は下記の「開発者による苦労話」を御覧ください。
・本番環境で課金が失敗した場合
本番環境で課金が失敗して多重課金等が発生したとしても、アップルが返金に応じてくれることはほとんどありません。本番環境で処理を失敗しないよう、十分注意しましょう。
実装コード例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
#import <StoreKit/StoreKit.h> #import "Subscription.h" #import "Base64EncDec.h" #import "JSON.h" @implementation Subscription // レシートの復元処理 - (NSDictionary *)verifyReceipt:(NSString *)base64Receipt { NSURL *url = [NSURL URLWithString:SUBSCRIPTION_URL]; 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) { return [status integerValue]; } // レシートデータをアプリ内に保存 NSDictionary *receiptDict = [dict objectForKey:@"latest_receipt_info"]; NSNumber *expired = [receiptDict objectForKey:@"expires_date"]; NSString *latestReceipt = [dict objectForKey:@"latest_receipt"]; // : completed = YES; return</code> <code>0; } // レシートのデコードと保存 - (BOOL)decodeReceiptWithTransaction:(NSString *)_receipt isRestore:(BOOL)_isRestore { NSDictionary *dict = [self verifyReceipt:_receipt]; NSInteger code = [self savePurchaseInfo:dict]; if (code != 0) { return</code> <code>NO; } else { if (_isRestore) { restored = YES; } return</code> <code>YES; } } // プロダクトの取得で呼び出されるデリゲート - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { if (response == nil) { return; } for (SKProduct *product in response.products ) { SKPayment *payment = [SKPayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } } - (void)failedTransaction:(SKPaymentTransaction *)transaction { if (!showedAlert) { showedAlert = YES; switch ([transaction.error code]) { case SKErrorUnknown: // 購入処理を中止 break; case SKErrorClientInvalid: // 不正なクライアント break; case SKErrorPaymentCancelled: // 購入処理をキャンセル break; case SKErrorPaymentInvalid: // 不正な購入 break; case SKErrorPaymentNotAllowed: // 購入が許可されていない break; default: break; } } } // 購入(リストア)トランザクション - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing: { // 購入中の処理 break; } case SKPaymentTransactionStatePurchased: { // 購入成功時の処理 [self decodeReceiptWithTransaction:[transaction.transactionReceipt stringEncodedWithBase64] isRestore:NO]; [queue finishTransaction:transaction]; break; } case SKPaymentTransactionStateFailed: { // 購入失敗時の処理 [self failedTransaction:transaction]; [queue finishTransaction:transaction]; break; } case SKPaymentTransactionStateRestored: { // 購入履歴復元時の処理 [self decodeReceiptWithTransaction:[transaction.transactionReceipt stringEncodedWithBase64] isRestore:YES]; [queue finishTransaction:transaction]; break; } } } // 購入(リストア)成功時の終了処理 if (restored || completed) { // : } } // リストア成功時の処理 - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { restored = YES; // : } // リストア失敗時の処理 - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { // : } // 課金処理(クライアントから呼び出されるメソッド) - (void) subscribe { completed = NO; [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; productIds = [NSSet setWithObject:"プロダクトID"]; skRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIds]; skRequest.delegate = self; [skRequest start]; } // リストア処理(クライアントから呼び出されるメソッド) - (void)restore { restored = NO; [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } - (void) dealloc { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; [super dealloc]; } @end |
まとめ
iOSアプリでの課金処理はiOS SDKに含まれているStoreKitと呼ばれるコンポーネントを利用するため一見簡単に実装できそうなのですが、Auto-Renewable Subscriptionsについては時間の経過により自動で課金されるという特性があるため、テストが非常にややこしくなっています。 一応、課金テストがやりやすいようにアップルがテスト環境(サンドボックス)を用意してくれているのですが、このテスト環境の仕様が全く明らかにされておらず、手探り状態でテストをする必要があります。
例えば、Auto-Renewable Subscriptionsでは課金タイミングを1週間〜1年の間で選択できるのですが、サンドボックスでは短い期間でテストできるよう、3分〜1時間で自動更新が実行されるようになっています。アプリの仕様により、購入の日時や更新の日時などを自社のサーバに保管し、アプリ内で利用する等した場合、この本番とサンドボックス環境とのタイミングの違いで、非常にややこしいテストケースを用意しないといけなかったり、シビアなタイミングが要求されたりします。
また、課金テストする際にはあらかじめテスト用ユーザ(AppleID)を用意するのですが、同じテストユーザで何度もAuto-Renewable Subscriptionsのテストを実行していると、自動継続がうまくいかなかったり、リストアが正常に終了しなかったりすることがあります。
さらにさらに、課金後にAppleから送られてくるレシート情報がサンドボックスと本番で微妙に違うことがあります。サンドボックスではうまくいっていたものが、いざ本番にアップすると失敗する・・・なんてこともまれにあります。
さらにさらにさらに、iOSのバージョンによるかもしれませんが、iPhoneとiPadでリストア(既に自動課金しているユーザが新しいデバイスやアプリの再インストールにより購読状態を復元させる処理)の動きが微妙に違ったりします。
とにかく、自動課金処理が全てブラックボックスになっており、仕様が不明瞭なため、Auto-Renewable Subscriptionsを利用する場合は念入りなテストケースと失敗した場合のリカバリの手段を用意しておく必要があります。
活用事例
Auto-Renewable Subscriptionsを利用しているアプリの一部を紹介します。
・産経新聞
※上記は当社が開発したアプリではありません。
まとめ
課金の部分は最もバグが怖い部分です。
弊社の実績のある課金処理部品化を使用することで、通常ならたくさんの時間をかけなければならないテストや処理を効率良く安心して使用することができます。