A Day In The Life

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

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

最近、無料アプリや無料ゲームにアプリ内課金を設置してユーザにアイテムを購入してもらうタイプのものが増えています。App Store トップセールスのうち半数以上がこの無料 + アプリ内課金で占められています。今後アプリ内課金は iPhone/iPad アプリで儲けるための必須の機能になると言っても過言ではありません。
今回はアプリ内課金(In-App Purchase)のプログラミングについて StoreKit フレームワークの基本的な使い方から失敗しないためのポイントまで説明していきます。

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

アプリ内課金で販売できるアイテムの種類は5種類あります。

  • Consumable
    消費アイテム。ユーザがアイテムを使うと無くなる。例えばシューティングゲームの弾丸やコイン落としゲームのコインなど。同じアイテムを何回でも購入可能
  • Non-Consumable
    非消費アイテム。一度購入したらずっと使えるアイテム。例えば広告解除や機能制限解除など。Apple ID に紐づいて購入履歴が管理されているため iPhone で購入したアイテムを iPad で使用するなんてこともできる。
  • Auto-Renewable Subscriptions
    自動継続型。期間を決めて課金するアイテム。
  • Free Subscription
    無料購読。
  • Non-Renewing Subscription
    購読型。

5つのうち下の3つは電子書籍やニュース系のアイテムのため今回は説明を省きます。Consumable と 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];
  }
}

3行目の if 文の判定は無効なアイテムがないかチェックする処理です。アイテム ID を間違えて指定した場合などに有効です。

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

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

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

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

アイテム購入処理

アイテム購入処理中は処理の状態が変わるごとに随時 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 アイテムは一度購入したら Apple ID が変わらない限りずっとアイテムを使用することができます。たとえばアプリを削除して再インストールした場合、アイテム販売者側は無料でアイテムを提供しなければいけません。この購入済みのアイテムを復活させる処理がリストアです。
リストアの処理は基本的には 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)で失敗しないためのポイント

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

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

アプリ内課金で販売できるアイテムと販売できないアイテムがあります。販売できないアイテムとしては以下が挙げられます。

  1. アダルト系
  2. リアルな商品
  3. レンタル形式のアイテム
  4. アプリをまたいで使用することができる仮想通貨
  5. 使用期限のある仮想通貨*2

一時期ゲーム内の仮想通貨は販売できないと言われていましたが4と5に抵触してなければ販売可能です。また仮想通貨に 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 になってからアプリ内課金のアイテムが有効になるまで2〜3時間かかります。その間の事故を防ぐことができます。またバグでアイテム ID を間違えて指定した場合にもこの判定が有効です。

SKErrorUnknown の処理をして2重課金事故のリスクを減らす

Apple ID にクレジットカード情報が入力されていない場合やクレジットカードのセキュリティコードが入力されていない場合、購入処理中に以下のダイアログが表示されます。
エラーメッセージ
「続ける」を押すと設定アプリのクレジットカード情報入力画面に移り、購入処理が強制的に終了してしまいます。このとき 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 アイテムの購入処理の復帰にも有効です。

関連

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

*1:NSOperation クラスに実装することも可能です

*2:日本国内で期限無しの仮想通貨を販売する場合、金融庁に届け出が必要です。ただし年間の売り上げが1000万円以下の場合届け出は不要です。

*3:規約にはありませんが Apple から直接指摘されました。今後変わる可能性があります。