失敗しない iOS In-App Purchase プログラミング
販売できるアイテムの種類は5種類
アイテム購入の流れ
アプリ内課金でアイテムを購入するときの流れは以下のようになっています。流れは Consumable, Non-Consumable ともに同じです。
- アプリ内課金が使えるかチェック
- アイテム情報の取得と購入処理の開始
- アイテム購入中の処理
- レシートの確認とアイテムの付与
- 購入処理の終了
Store Kit フレームワークの概要
![StoreKit フレムワーククラス図](http://cdn-ak.f.st-hatena.com/images/fotolife/g/glass-_-onion/20111207/20111207085241.png)
![StoreKit実装クラス図](http://cdn-ak.f.st-hatena.com/images/fotolife/g/glass-_-onion/20111203/20111203185052.png)
@interface HogeViewController : UIViewController <SKProductsRequestDelegate, SKPaymentTransactionObserver> { } @end
アプリ内課金が使えるかチェック
![App内での購入をオフにする](http://cdn-ak.f.st-hatena.com/images/fotolife/g/glass-_-onion/20111213/20111213121431.png)
プログラムは以下になります。
if (![SKPaymentQueue canMakePayments]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:@"アプリ内課金が制限されています。" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; [alert release]; return; }
アイテム情報の取得と購入処理の開始
それではアイテムの情報を取得してから購入処理を開始するまでの動きを見ていきましょう。
アイテム情報を取得する
NSSet *set = [NSSet setWithObjects:@"com.commonsense.removeads", nil]; SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set]; productsRequest.delegate = self; [productsRequest start];1行目でアイテム ID(com.commonsense.removeads)を渡しています。複数のアイテムIDを渡すことも可能です。
購入処理の開始
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { // 無効なアイテムがないかチェック if ([response.invalidProductIdentifiers count] > 0) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:@"アイテムIDが不正です。" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [alert show]; [alert release]; return; } // 購入処理開始 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; for (SKProduct *product in response.products) { SKPayment *payment = [SKPayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } }3行目のif文の判定は無効なアイテムがないかチェックする処理です。アイテムIDを間違えて指定した場合などに有効です。
アイテム購入中の処理から購入処理終了まで
アイテムの購入処理が開始されてから購入処理が終わるまでの動きについて説明していきます。
アイテム購入処理
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { if (transaction.transactionState == SKPaymentTransactionStatePurchasing) { // 購入処理中 /* * 基本何もしなくてよい。処理中であることがわかるようにインジケータをだすなど。 */ } else if (transaction.transactionState == SKPaymentTransactionStatePurchased) { // 購入処理成功 /* * ここでレシートの確認やアイテムの付与を行う。 */ [queue finishTransaction:transaction]; } else if (transaction.transactionState == SKPaymentTransactionStateFailed) { // 購入処理エラー。ユーザが購入処理をキャンセルした場合もここにくる [queue finishTransaction:transaction]; // エラーが発生したことをユーザに知らせる UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:[transaction.error localizedDescription] delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; [alert release]; } else { // リストア処理完了 /* * アイテムの再付与を行う */ [queue finishTransaction:transaction]; } } }
レシートの確認とアイテムの付与
//NSURL *url = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"]; NSURL *url = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; NSString *json = [NSString stringWithFormat:@"{\"receipt-data\" :\"%@\"}", [transaction.transactionReceipt stringEncodedWithBase64]]; [request setHTTPBody:[json dataUsingEncoding:NSUTF8StringEncoding]]; NSURLResponse *response; NSError *error; NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);サーバ側の処理は様々な言語で書かれていると思うので適宜他の言語に読み替えて実装してください。
アイテムのリストア
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];1アイテムのリストアの処理が終わるごとに paymentQueue:removedTransactions: が呼ばれます。全てのリストア処理が終了すると paymentQueueRestoreCompletedTransactionsFinished: メソッドが呼ばれます。 以下はリストア処理を行うプログラムの例です。
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { if (transaction.transactionState == SKPaymentTransactionStatePurchasing) { // 購入処理中 } else if (transaction.transactionState == SKPaymentTransactionStateRestored) { // リストア処理完了 /* * アイテムの再付与を行う */ [queue finishTransaction:transaction]; } } } - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { // リストアの失敗 } - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { // 全てのリストア処理が終了 }アイテムのリストア処理の動きを図にすると以下のようになります。
![リストア処理](http://cdn-ak.f.st-hatena.com/images/fotolife/g/glass-_-onion/20111201/20111201083757.png)
サンドボックス環境で課金のテストをする
プログラム開発が終わったらサンドボックス環境でテストしましょう。サンドボックス環境では本番のアイテム購入と似た環境でテストすることができます。サンドボックス環境でアイテムを購入してもお金の請求はされません。
サンドボックス環境でテストを行う手順は以下の通りです。
- テストユーザの作成
- 端末の Apple ID をサインアウト
- 開発版アプリをインストール
- テスト
テストユーザの作成
開発中のアプリでアイテム購入のテストをするためには iTunes Connect でテストユーザを作成する必要があります。テストユーザは iTunes Connect メニューの Manage Users の Test User から作成することができます。
端末の Apple ID をサインアウト
テストユーザが作成できたら iPhone または iPad の設定アプリの Store を選択しください。一番下のApple ID: を選択してサインアウトしてください。
開発版アプリをインストール
開発版アプリをインストールしてアイテムの購入処理を行うとテストができます。アイテムを購入すると Apple ID の入力を求められるので手順1で作成したテストユーザの ID とパスワードを入力してください。
本番環境で課金のテストをする
サンドボックス環境は正常系のテストしかできないのでアプリがリリースされたら必ず本番環境でテストしましょう(お金がかかりますが仕方ありません)。本番環境でテストする時は開発版のアプリを削除して App Store からアプリをダウンロードしてテストしてください。アプリが開発版のままだとテストができないので注意が必要です。
アプリ内課金(In-App Purchase)で失敗しないためのポイント
アプリ内課金で失敗しないためのポイントをいくつか挙げてみます。
販売できないアイテムがある
アプリ内課金が使えるかチェックする
SKPaymentQueue の canMakePayments メソッドを使ってユーザがアプリ内課金を使えるかきちんとチェックしましょう。
アイテムの無効判定をきちんと行う
SKErrorUnknown の処理をして2重課金事故のリスクを減らす
![エラーメッセージ](http://cdn-ak.f.st-hatena.com/images/fotolife/g/glass-_-onion/20111208/20111208113019.png)
﹁続ける﹂を押すと設定アプリのクレジットカード情報入力画面に移り、購入処理が強制的に終了してしまいます。このとき SKPaymentTransactionObserver の処理はエラー内容に SKErrorUnknown がセットされてSKPaymentTransactionStateFailed で終了してしまいます。 ユーザが設定アプリのクレジットカード情報を入力し処理を続行すると再び購入ダイアログが表示されます。そこでダイアログの﹁購入する﹂ボタンを押すと次の購入処理までトランザクションが残ってしまいます。トランザクションが残ったまま別の購入処理を行うと前回のトランザクションが並行で処理され最悪の場合、2重課金事故が発生します。 この現象はサンドボックス環境では発生しないためユーザから問い合わせが来るまで気づかないことが多いです。 Non-Consumable アイテムは一度購入したら次から無料で購入できるので2重課金されることはありませんが、Consumable アイテムは SKErrorUnknown で終了した処理を復帰しないと2重課金される場合があります。 SKErrorUnknown で処理が終了したトランザクションの復帰処理は SKPaymentQueue の restoreCompletedTransactions メソッドで行います。以下はそのコード例です。
- (void)viewDidLoad { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; } - (void)applicationDidEnterBackground { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; } - (void)applicationWillEnterForeground { [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { if (transaction.transactionState == SKPaymentTransactionStatePurchasing) { // 省略 } else if (transaction.transactionState == SKPaymentTransactionStatePurchased) { // 省略 } else if (transaction.transactionState == SKPaymentTransactionStateFailed) { [queue finishTransaction:transaction]; if (transaction.error.code == SKErrorUnknown) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"処理が途中中断されました" message:[transaction.error localizedDescription] delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; [alert release]; } } else { // 復帰処理完了 /* * アイテムの付与処理 */ [queue finishTransaction:transaction]; } } } - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { // 途中で止まった処理を再開する Consumable アイテムにも有効 [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; }ポイントは UIApplicationDidEnterBackgroundNotification を監視してアプリがバッググラウンドに入ったタイミングでオブザーバの監視を止めるところです。このようにすることで Consumable アイテムであっても restoreCompletedTransactions できるようになります。 一般的に restoreCompletedTransactions は Non-Consumable アイテムの復帰に使うと思われがちですが途中中断した Consumable アイテムの購入処理の復帰にも有効です。