[iOS]CargoBayを使用したアプリ内課金 (各種処理編)

2016.04.04

1 はじめに

アプリ内課金には、下記の5つのタイプがあり、用途に応じて適切なものを選択する必要がります。

タイプ 再購入の可否 リストア機能 リストアAPI 使用例
消耗型(Consumable) ○(何度でも) 不要 ✖︎ 釣り餌
非消耗型(Non-consumable) ✖(できない)︎ 必要 ○︎ ゲームの機能解放
自動更新登録(Auto-renewable subscriptions) ✖(できない)︎自動的に更新される 必要 ○︎ 定期登録
非更新登録(Non-renewable subscriptions) ○(更新しますか) 必要 ︎✖︎ 年間サブスクリプション
無料登録(Free) ○(無料を使用しますか) 必要 ︎○ 無料の会員登録

リストア機能が不要な「消耗型」であれば、簡単に実装できるのですが、それ以外の場合は、リストア機能の実装が必須となっており、やや敷居が高くなります。

リストア機能が必要かどうかは、アップルのドキュメントに次のように記載されています。
Getting Started with In-App Purchase on iOS and OS X Version 3.1

008

なお、リストア機能についてですが、リストアAPIを使用すると、アップルIDに紐付いた購入歴が簡単に取れますので、そんなに難しくはありません。 しかし、リストアAPIが使用できない「非更新登録」だけは、自前で実装する必要があります。(上記にも、iCloud若しくは、自前サーバで実装するように書かれています。)

2 CargoBay

CargoBayは、StoreKitフレームワークによるアプリ内課金の処理をラップして、Block形式で扱いやすくしたライブラリです。

このライブラリを使用することで、複雑なStoreKitフレームワークを比較的簡単に扱う事ができます。特に、「レシートの照会」などは、このライブラリを導入することで格段に簡単になります。

CargoBayは、MITライセンスで公開されており、CocoaPodで簡単にインストールが可能です。 (依存関係からAFNetworkingも一緒に組み込まれます)

pod 'CargoBay'

参考:CocoaPodsによる、外部ライブラリの利用と作成

なお、2016年3月現在、最新は、2.1.1です。 ライブラリの導入後は、下記のインポートで利用可能になります。

#import "CargoBay.h"

以下、このCargoBayを使用した、App内課金の各種の処理について確認していきます。

3 アプリ内課金の各種処理

(1) Ovserver登録とレスポンス取得

アプリ内課金は、StoreKitフレームワークを使用して記述します。 同フレームワークは、非同期で動作するトランザクション機能で、App Storeとやり取りを行います。

アプリケーション側から見ると、リクエストをキューに積んでレスポンスを待つという形になりますが、 このレスポンスを受け取るために、SKPaymentTransactionObserverプロトコルを実装したクラスを登録しなければなりません。

CargoBayは、このSKPaymentTransactionObserverプロトコルを実装しており、CargoBay自身を、オブザーバーとして登録することで、レスポンスを受け取ることができます。

下記は、CargoBayのオブザーバー登録と、レスポンスを受けた際の処理をブロックで記述している例です。

なお、In-App Purchase プログラミングガイドによると、このObserverの登録はアプリケーションの起動時に行うように指示されているため、application:didFinishLaunchingWithOptions:に記述しています。

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // レスポンスを受け取る
    [[CargoBay sharedManager] setPaymentQueueUpdatedTransactionsBlock:^(SKPaymentQueue *queue, NSArray *transactions) {
        for (SKPaymentTransaction *transaction in transactions){ // 複数のトランザクションを受け取る場合がある

            NSArray *statusString = @[@"Purchasing",@"Purchased",@"Failed",@"Restored",@"Deferred"];
            NSString *log = [NSString stringWithFormat:@"\n>レスポンス Status=%@",statusString[transaction.transactionState]];
            [_viewController appendLog:log]; // ログの追加

            switch (transaction.transactionState) { // ステータスによって処理を分岐する
                case SKPaymentTransactionStatePurchasing:
                    //進行中
                    break;
                case SKPaymentTransactionStatePurchased:
                    //購入処理
                    [queue finishTransaction:transaction]; // トランザクションの終了
                    break;
                case SKPaymentTransactionStateFailed:
                    //失敗
                    [queue finishTransaction:transaction]; // トランザクションの終了
                    break;
                case SKPaymentTransactionStateRestored:
                    //リストア
                    [queue finishTransaction:transaction]; // トランザクションの終了
                    break;
                case SKPaymentTransactionStateDeferred:
                    //エラー
                    [queue finishTransaction:transaction]; // トランザクションの終了
                    break;
            }
        }
    }];

    //オブザーバー登録
    [[SKPaymentQueue defaultQueue] addTransactionObserver:[CargoBay sharedManager]];

    return YES;
}

トランザクション終了時には、これを明示的に終了させる責任がアプリ側にあります。上の例では、ステータスがSKPaymentTransactionStatePurchasing(進行中)以外の場合に、終了処理を入れています。

(2) プロダクト情報の問合わせ

productsWithIdentifiers:を使用してプロダクト情報の問い合わせを行います。プロダクト情報は、自ら登録した識別子なので特に確認する必要は無いように感じますが、なんらかの理由でストアで無効になっている可能性もありますので、この処理は必ず必要です。

次の例では、プロダクトID3つのうち、最初の1つだけが有効で、あとは無効なIDを指定してみました。

NSArray *identifiers = @[
             @"jp.ne.sapporoworks.CargoBaySample.Consumable",
             @"jp.ne.sapporoworks.foo",
             @"jp.ne.sapporoworks.bar"];

[[CargoBay sharedManager] productsWithIdentifiers:[NSSet setWithArray:identifiers]
                                          success:^(NSArray *products, NSArray *invalidIdentifiers) {
                                              NSLog(@"Products: %@", products);
                                              NSLog(@"Invalid Identifiers: %@", invalidIdentifiers);
                                          } failure:^(NSError *error) {
                                              NSLog(@"Error: %@", error);
                                          }];

実行時の出力は、次の通りです。success:のブロックで、有効(products)と無効(invalidIdentifiers)が受け取れます。(通信障害などの場合はfailure:ブロックに落ちます。)

Products: (
    "<SKProduct: 0x13d60b4f0>"
)
Invalid Identifiers: (
    "jp.ne.sapporoworks.foo",
    "jp.ne.sapporoworks.bar"
)

CargoBayの内部では、概ね下記のような感じで、StoreKitフレームワークSKProductsRequestを指定したIDで初期化してstartでプロダクト情報を問い合わせています。

SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers];
request.delegate = self;
[request start];

(3) 購入

プロダクトIDで初期化したSKPaymentSKPaymentQueueに追加することでトランザクションが始まります。

ここで使用するプロダクトは、先の「(2) プロダクト情報の問合わせ」で取得できたものを使用します。

SKPayment *payment = [SKPayment paymentWithProduct:_product];
[[SKPaymentQueue defaultQueue] addPayment:payment];

購入のトランザクションは、非同期で実行され、UI上は、ストアへのログインや購入確認を経て、最終的にレスポンスブロックで受け取ります。

002

001

ステータスがSKPaymentTransactionStatePurchased:の場合、購入完了なのでで、次に説明する「(4)トランザクションの確認」に進めます。 購入が成立しなかった場合は、ステータスが「失敗」や「エラー」となってレスポンスが返ります。

[[CargoBay sharedManager] setPaymentQueueUpdatedTransactionsBlock:^(SKPaymentQueue *queue, NSArray *transactions) {

    for (SKPaymentTransaction *transaction in transactions){
        switch (transaction.transactionState) {
        
          //・・・省略

            case SKPaymentTransactionStatePurchased://購入

                // 「(4)トランザクションの確認」の処理をここに記述する

                [queue finishTransaction:transaction]; // トランザクションの終了
                break;
        
        //・・・省略

(4) トランザクションの確認

レスポンスで受け取ったSKPaymentTransactionを使用して、トランザクションの確認を実施します。 具体的には、CargoBayに用意されていうるverifyTransaction:をコールするだけです。

結果や非同期で受け取ることになります。

結果を確認して、問題なければ、アプリ的に課金完了の処理を実行します。

pasuword:というパラメータは、「自動更新登録」の場合だけ、必要になります。

// 購入完了の処理
- (void) purchased:(SKPaymentTransaction *)transaction {
    [self appendLog:@"\n>購入完了"];

    [[CargoBay sharedManager] verifyTransaction:transaction password:nil success:^(NSDictionary *json) {

        NSLog(json.description);

        NSString *status = ;
        [self appendLog:@"\nStatus:"];
        [self appendLog:status];

        NSDictionary *receipt = ;

        NSString *product_id = [receipt objectForKey:@"product_id"];
        [self appendLog:@"\nproduct_id:"];
        [self appendLog:product_id];

        NSString *purchase_date_ms = [receipt objectForKey:@"purchase_date_ms"];
        [self appendLog:@"\npurchase_date_ms:"];
        [self appendLog:purchase_date_ms];

        NSString *purchase_date = [receipt objectForKey:@"purchase_date"];
        [self appendLog:@"\npurchase_date:"];
        [self appendLog:purchase_date];

        NSString *unique_identifier = [receipt objectForKey:@"unique_identifier"];
        [self appendLog:@"\nunique_identifier:"];
        [self appendLog:unique_identifier];

        NSString *unique_vendor_identifier = [receipt objectForKey:@"unique_vendor_identifier"];
        [self appendLog:@"\nunique_vendor_identifier:"];
        [self appendLog:unique_vendor_identifier];

        // アプリ的な課金完了処理は、ここに記述する

    } failure:^(NSError *error) {
        [self appendLog:@"\n>Error:"];
        [self appendLog:error.description];
    }];
}


(5) リストア

下記の1行でアップルIDに紐付いている購入済みのプロダクト情報が取得できます。 なお、繰り返しになりますが、取得できるのは、最初の表で「リストアAPI」のところが「○」なっている「非消耗型」「自動更新登録」「無料登録」の3タイプだけです。

[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

上記のトランザクションのレスポンスは、ステータスは、SKPaymentTransactionStateRestoredで返ります。 そして、この時transactionsの配列に、有効な全てのプロダクトが入っています。

実装としては、ここにも、購入完了の時に同じように、「(4)トランザクションの確認」の処理へ進めばいいだけです。

[[CargoBay sharedManager] setPaymentQueueUpdatedTransactionsBlock:^(SKPaymentQueue *queue, NSArray *transactions) {

    for (SKPaymentTransaction *transaction in transactions){
        switch (transaction.transactionState) {

            // ・・・省略

            case SKPaymentTransactionStateRestored://リストア

                 // 「(4)トランザクションの確認」の処理をここに記述する

                [queue finishTransaction:transaction]; // トランザクションの終了
                break;

           // ・・・省略

4 コンテンツ購入の許可

上記までの処理とは別に、アプリ内課金の処理を記述する前に、端末でコンテンツの購入の許可されているかどうかを確認するしなければなりません。 下記が、その例です。

// アプリ内でコンテンツの購入を許可されているかどうかの確認
if ( [SKPaymentQueue canMakePayments] == NO){
    [RMUniversalAlert showAlertInViewController:self
                                        withTitle:nil
                                        message:@"機能制限でApp内の購入が不可になっています。[設定]>[一般]>[機能制限]>[App内での購入]をONにいていただいたうえ再度お試しください。"
                                cancelButtonTitle:@"OK"
                            destructiveButtonTitle:nil
                                otherButtonTitles:nil
                                        tapBlock:^(RMUniversalAlert *alert, NSInteger buttonIndex){
                                        }];
    return;
}

5 最後に

今回は、CargoBayを使用したアプリ内課金の各種処理について、確認しました。 次回、「実装編」では、これらの処理を組み合わせて、5タイプのアプリ内課金を実装してみたいと思います。

6 参考資料


https://github.com/mattt/CargoBay
https://cocoapods.org/pods/CargoBay
[iOS] CargoBayを使用したアプリ内課金(実装編)