商品の登録

  • iTunes Connectからアプリを選択して詳細画面表示
  • Manage in App Purchasesというボタンがあるので押す
  • Create Newボタンを押す
  • いくつか商品タイプが表示される
  • 広告非表示のように1回購入したら永続的な制限解除はNon-Consumableを選択
  • Reference Nameにわかりやすい名前をいれる(内部的な区分用、ユーザーには表示されない)
  • Product IDに他のアプリ、商品と区別できる任意の文字列を入れる。Bundle Identifier + 商品IDが良いと思う。例)net.stack3.passionz_for_tumblr.remove_ads
  • Price Tierで商品価格を選択
  • Add Languageボタンを押して指定の言語でのDisplay Name(表示名)、Description(説明)を入れてSaveを押す
  • Screenshot for ReviewのChoose Fileを押して、商品が反映された状態のアプリのスクリーンショットを表示する。あくまでレビュー用でユーザーには公開されない。広告非表示なら広告非表示状態のスクリーンショットを送れば良いと思う。

テストユーザーの登録

開発環境で課金処理を実行するとSandboxというテスト用のiTunes Storeサーバーへ接続しに行く。その時に正規ストア接続の流れと同様にApple IDとパスワードを求められるが、テストユーザーでないと認証に失敗する。よってテストユーザーの登録が必要。ちなみにテストサーバーでは商品購入しても課金処理はされないので安心。

  • iTunes ConnectからManage Usersを選択
  • Test Userを選択
  • Add New Userボタンを押す
  • First Name,Email Addressなどすべての項目を埋める
  • Saveボタンを押して作成

あとはテストユーザーでログインできるようにサインアウトしておく必要があります。そのままだとiOSで設定されているApple IDは変更できずパスワードのみの入力になるためです。

  • iOSの設定からStoreを選択
  • 設定されているApple IDを選択して、サインアウトを押す

このとき注意するべきなのは、iOSの設定からテストユーザーを設定してはならないということ。正規のユーザー登録の流れに入ってしまうためです。間違って設定しようとしたらキャンセルしましょう。

商品名を得る

商品や商品説明はプログラムで持つのではなく、iTunes Connectで登録した情報をサーバーから読み込むほうがメンテを考えると得策だろう。

SKProductsRequest#startで指定したProduct IDの商品情報の取得を開始

_request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIds];
_request.delegate = self;
[_request start];

成功とエラーのdelegateメソッドを実装

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    if (response == nil) {
        NSLog(@"Response is empty.")
        return;
    }
    
    // 無効なproduct IDが渡された時、その一覧が返される
    for (NSString *identifier in response.invalidProductIdentifiers) {
        NSLog(@"Invalid product identifier: %@", identifier);
    }
    
    // 商品情報(SKProduct)はresponse.productsに配列で入っている
    for (SKProduct *product in response.products) {
        NSLog(@"Product %@", product);
    }
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    NSLog(@"Error: %@", error);
}

取得した商品情報をもとにUITableViewなどに一覧表示すると良いだろう。

商品名はSKProduct#localizedTitle、商品説明はSKProduct#localizedDescription、価格はSKProduct#priceで取得できる。priceはNSDecimalNumber型になっていて、フォーマットして表示する必要がある。SKProductのDocumentにその方法は載っている。SKProductのカテゴリメソッドにしておくと良いかもしれない。

@implementation SKProduct (カテゴリ名)

- (NSString*)localizedPriceString {
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
    [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    [numberFormatter setLocale:self.priceLocale];
    return [numberFormatter stringFromNumber:self.price];
}

@end

商品の購入処理

注意するべきことは商品の購入処理はアプリケーション内部というよりOSレベルで行われるということ。アプリケーションがバックグラウンドでも購入処理が走る。

SKPaymentQueueのObserverを設定する

購入処理の購入完了、キャンセル、エラーはViewControllerはなく、Applicationレベルで管理する必要がある。AppDelegateなどがSKPaymentQueueのObserverになると良いだろう。

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

Observerとなったクラスは購入処理の通知を受け取り適宜処理する。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        if (transaction.transactionState == SKPaymentTransactionStatePurchasing) {
            NSLog(@"payment purchasing.");
        } else if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
            NSLog(@"payment transaction.payment.productIdentifier : %@",transaction.payment.productIdentifier);
            [queue finishTransaction: transaction];
            
            // ここでNSNotificationを投げると良いだろう
        } else if (transaction.transactionState == SKPaymentTransactionStateFailed) {
            // Also come here if user canceled.
            [queue finishTransaction:transaction];
            
            // transaction.payment -> SKPayment
            // transaction.error.code == SKErrorPaymentCancelled -> Cancelしたときのエラーコード
            if (!transaction.error.code == SKErrorPaymentCancelled) {
                NSLog(@"payment error : %@", transaction.error.localizedDescription);
            } else {
                NSLog(@"payment transaction is canceled");
            }
            
            // ここでNSNotificationを投げると良いだろう
        } else if (transaction.transactionState == SKPaymentTransactionStateRestored) {
            // Restore
            NSLog(@"payment transaction.originalTransaction.payment.productIdentifier : %@",transaction.originalTransaction.payment.productIdentifier);
            [queue finishTransaction:transaction];
            
            // ここでNSNotificationを投げると良いだろう
        }
    }
}

購入処理のキャンセルはプログラムからはできないようである。よって一度、購入処理が走ったら他の画面に遷移しても、購入処理の監視が必要である。後述するがAppDelegateなどで購入処理を管理すべきでViewControllerがObserverになるのは望ましくないと思う。

購入処理の開始

以下のようにSKProductからSKPaymentを生成してSKPaymentQueue#addPaymentするだけ。

SKProduct *prodct = XXXX;
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];

ちなみに購入するかどうかの確認アラートはaddPayment呼び出し後、自動で表示される。よってアプリ側では購入前の確認アラート表示はしなくても良いと思う。SimulatorだとaddPaymentを読んでから数秒後に確認アラートが表示される。この間にアプリをバックグラウンドにしたり、画面遷移ができる。 アプリをバックグラウンドした状態で、確認アラートで購入やキャンセルをし、再びアプリをフォアグラウンドにするとpaymentQueue:updatedTransactions:が呼び出される。このように購入処理はiOSレベルで行われていることが確認できる。それに注意して実装するべきである。

購入完了後のアラート表示は、商品一覧を表示しているViewControllerではなく、paymentQueue:updatedTransactions:でSKPaymentTransactionStatePurchasedが来た時にAppDelegateなどで表示するほうが良い。ViewControllerは画面遷移したらオブジェクトが消えるためアラート表示できない。paymentQueue:updatedTransactions:による購入処理の監視と購入完了後の処理はAppDelegateなどアプリがメモリにある間、存在し続けるクラス内で行うべきだろう。

購入したかどうかの状態保存

購入完了を受け取った後、アプリ内で購入したことを保存する必要がある。NSUserDefaultsだとハックが容易なので避けたほうが良いかもしれない。自分はキーチェインに保存するようにしている。値もYES、NOではなくパスワードのように複雑な文字列の組み合わせにしている。