A Day In The Life

とあるプログラマの備忘録

失敗しない iOS In-App Purchase プログラミング


App Store  +  iPhone/iPad 
(In-App Purchase) StoreKit 使

販売できるアイテムの種類は5種類


5


Consumable
使

Non-Consumable
使AppleID iPhone  iPad 使

Auto-Renewable Subscriptions


Free Subscription


Non-Renewing Subscription



53Consumable  Non-Consumable 

アイテム購入の流れ

アプリ内課金でアイテムを購入するときの流れは以下のようになっています。流れは Consumable, Non-Consumable ともに同じです。

  1. アプリ内課金が使えるかチェック
  2. アイテム情報の取得と購入処理の開始
  3. アイテム購入中の処理
  4. レシートの確認とアイテムの付与
  5. 購入処理の終了

Store Kit フレームワークの概要


 StoreKit 使StoreKit 2







StoreKit 
StoreKit フレムワーククラス図 StoreKit 


SKProductsRequestDelegate


SKPaymentTransactionObserver



2 UIViewController *1

StoreKit実装クラス図
@interface HogeViewController : UIViewController <SKProductsRequestDelegate, 
                                            SKPaymentTransactionObserver> {
}
@end

アプリ内課金が使えるかチェック


使使
App内での購入をオフにする

if (![SKPaymentQueue canMakePayments]) {
  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー"
                                                  message:@"アプリ内課金が制限されています。"
                                                 delegate:nil
                                        cancelButtonTitle:nil
                                        otherButtonTitles:@"OK", nil];
  [alert show];
  [alert release];
  return;
}

アイテム情報の取得と購入処理の開始

それではアイテムの情報を取得してから購入処理を開始するまでの動きを見ていきましょう。

アイテム情報を取得する

使 SKProductsRequest 
SKProductsRequest  start 

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
購入処理の開始

 productsRequest:didReceiveResponse: response 使 SKPaymentQueue  addPayment 

- (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];
  }
}

3ifID
ここまでの流れをまとめると

アイテムの情報取得から購入処理開始までのプログラムの流れを図にすると以下のようになります。

アイテム購入中の処理から購入処理終了まで

アイテムの購入処理が開始されてから購入処理が終わるまでの動きについて説明していきます。

アイテム購入処理

 paymentQueue:updatedTransactions: 

- (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];
    }
  }  
}
レシートの確認とアイテムの付与


3










 paymentQueue:updatedTransactions:  transaction  Base64 iTunes 
 Objective-C 
//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]);


購入処理の終了

全てのトランザクションが終了すると paymentQueue:removedTransactions: メソッドが呼ばれます。このメソッドでオブザーバの削除を行います。

- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions 
{
  [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
ここまでの流れをまとめると

アイテムの購入処理から購入処理の終了までの流れを図にすると以下のようになります。
アイテム購入処理
レシート確認とアイテム付与は上記の図から省略しています。実際には購入処理終了アクティビティ後の updatedTransactions アクティビティの処理で行うことになります。

アイテムのリストア


Non-Consumable  AppleID使
 Non-Consumable 使 Consumable  SKErrorUnknown 
 SKPaymentQueue  restoreCompletedTransactions 
[[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 
{
  // 全てのリストア処理が終了
}


リストア処理

サンドボックス環境で課金のテストをする

プログラム開発が終わったらサンドボックス環境でテストしましょう。サンドボックス環境では本番のアイテム購入と似た環境でテストすることができます。サンドボックス環境でアイテムを購入してもお金の請求はされません。
サンドボックス環境でテストを行う手順は以下の通りです。

  1. テストユーザの作成
  2. 端末の Apple ID をサインアウト
  3. 開発版アプリをインストール
  4. テスト
テストユーザの作成

開発中のアプリでアイテム購入のテストをするためには iTunes Connect でテストユーザを作成する必要があります。テストユーザは iTunes Connect メニューの Manage Users の Test User から作成することができます。

端末の Apple ID をサインアウト

テストユーザが作成できたら iPhone または iPad の設定アプリの Store を選択しください。一番下のApple ID: を選択してサインアウトしてください。

開発版アプリをインストール

開発版アプリをインストールしてアイテムの購入処理を行うとテストができます。アイテムを購入すると Apple ID の入力を求められるので手順1で作成したテストユーザの ID とパスワードを入力してください。

テスト

サンドボックス環境は本番と全く同じ環境ではありません。本番でしか起こらないエラーや画面遷移があります。サンドボックスのテストは正常系の確認ができる程度のテストしかできないと考えておきましょう。
とは言え、アプリを正式リリースするまではサンドボックス環境でしかテストできないので初回リリースの時はかなりハラハラします。

本番環境で課金のテストをする

サンドボックス環境は正常系のテストしかできないのでアプリがリリースされたら必ず本番環境でテストしましょう(お金がかかりますが仕方ありません)。本番環境でテストする時は開発版のアプリを削除して App Store からアプリをダウンロードしてテストしてください。アプリが開発版のままだとテストができないので注意が必要です。

本番環境でアプリをデバックする

アイテム課金対応のアプリがリリースされたあと限定ですが本番環境でアプリをデバッグすることができます。App Store からアプリをダウンロードして上書きで開発用アプリをインストールすると本番環境でデバッグしながらテストすることができます(実際の課金処理がはしります)。
エラー系のテストはこの方法でテストすることをおすすめします。

アプリ内課金(In-App Purchase)で失敗しないためのポイント

アプリ内課金で失敗しないためのポイントをいくつか挙げてみます。

販売できないアイテムがある




(一)

(二)

(三)

(四)使

(五)使*2


45 Tier 60 (8500)*3
 App Store Review Guidelines 11. Purchasing and currencies App Store Review Guidelines 
App Store Review Guidelines 
アプリ内課金が使えるかチェックする

SKPaymentQueue の canMakePayments メソッドを使ってユーザがアプリ内課金を使えるかきちんとチェックしましょう。

アイテムの無効判定をきちんと行う

productsRequest:didReceiveResponse: SKProductsResponse  invalidProductIdentifiers 
 Ready for Sale 23ID
SKErrorUnknown の処理をして2重課金事故のリスクを減らす

AppleID
エラーメッセージ
 SKPaymentTransactionObserver  SKErrorUnknown SKPaymentTransactionStateFailed 
2

Non-Consumable 2Consumable  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 

関連

この記事は iOS Advent Calendar 2011 12月1日の記事です。


*1:NSOperation 

*2:1000

*3: Apple