プロダクトの配信

購入プロセスの最後の段階では、App Storeによる支払い要求の処理後、Appは今後の起動に備えて情報を保存し、購入したコンテンツをダウンロードして、トランザクションに終了のマークを付けます(図 5-1を参照)。

図 5-1 購入プロセスの各段階—プロダクトの配信

App Storeがトランザクションを処理するのを待機する

トランザクションキューは、StoreKitフレームワークを介してAppとApp Storeが通信するときに中心的な役割を演じます。処理する必要がある支払い要求など、App Storeによる処理が必要な作業をこのキューに追加します。トランザクションの状態が、たとえば、支払い要求に成功して変化した場合、StoreKitはAppのトランザクションキューのオブザーバを呼び出します。オブザーバとして振る舞うクラスは開発者が決めます。非常に小規模なAppであれば、トランザクションキューの監視その他、StoreKitに関係する処理をすべてAppデリゲートで扱っても構いません。しかし多くの場合、オブザーバとしての挙動をはじめ、ストアに関係する処理をおこなう独立したクラスを用意する方がよいでしょう。オブザーバにはSKPaymentTransactionObserverプロトコルを実装する必要があります。

オブザーバを使用する場合、Appではアクティブなトランザクションを常時ポーリングしているわけではありません。Appでは、支払い要求のほか、Appleによってホストされたコンテンツのダウンロードとサブスクリプションの更新の検出にもトランザクションキューを使用します。

Appの起動時に、トランザクションキューのオブザーバを登録します(リスト 5-1を参照)。オブザーバは、キューにトランザクションを追加した直後だけでなく、いつでもトランザクションを処理できることを確認します。たとえば、トンネルに入る直前にユーザーがAppで何かを購入する場合について考慮します。ネットワーク接続がないため、Appは購入されたコンテンツを配信できません。Appが次回起動されたときに、StoreKitはトランザクションキューのオブザーバを再度呼び出して、購入された項目をその時点で配信します。同様に、Appがトランザクションに終了のマークを付けられなかった場合、トランザクションが終了したと適切にマークされるまで、StoreKitはAppが起動されるたびに毎回オブザーバを呼び出します。

リスト 5-1 トランザクションキューのオブザーバの登録

- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    /* ... */
 
    [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}

トランザクションキューのオブザーバにpaymentQueue:updatedTransactions:を実装します。トランザクションの状態が、たとえば支払い要求が処理されたために変化すると、StoreKitはこのメソッドを呼び出します。トランザクションの状態は、表 5-1およびリスト 5-2に示すように、Appで実行する必要があるアクションがどれであるのかを通知します。キューに入っているトランザクションの状態が変わる順序は不定です。Appはいつでもアクティブなトランザクションを処理できるようにしておかなければなりません。

表 5-1 トランザクションの状態と対応するアクション

状態

Appで実行するアクション

SKPaymentTransactionStatePurchasing

進捗している状態を反映するようにUIを更新し、再び呼び出されるのを待機します。

SKPaymentTransactionStateDeferred

遅延している状態を反映するようにUIを更新し、再び呼び出されるのを待機します。

SKPaymentTransactionStateFailed

errorプロパティの値を使用して、ユーザーにメッセージを表示します。エラーを表す定数については、SKErrorDomainに関する項に一覧が載っています。

SKPaymentTransactionStatePurchased

購入した機能を提供します。

SKPaymentTransactionStateRestored

以前に購入した機能を復元します。

リスト 5-2 トランザクションの状態への応答

- (void)paymentQueue:(SKPaymentQueue *)queue
 updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            // トランザクションの状態に応じた、適切なカスタムメソッドを呼び出し。
            case SKPaymentTransactionStatePurchasing:
                [self showTransactionAsInProgress:transaction deferred:NO];
                break;
            case SKPaymentTransactionStateDeferred:
                [self showTransactionAsInProgress:transaction deferred:YES];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            default:
                // デバッグのため
                NSLog(@"Unexpected transaction state %@", @(transaction.transactionState));
                break;
        }
    }
}

待機中にユーザーインターフェイスを最新の状態に保つには、トランザクションキューのオブザーバはSKPaymentTransactionObserverプロトコルから次のようにオプションメソッドを実装できます。paymentQueue:removedTransactions:メソッドは、トランザクションがキューから削除されると呼び出されます。このメソッドの実装では、対応する項目をAppのUIから削除します。paymentQueueRestoreCompletedTransactionsFinished:メソッドまたはpaymentQueue:restoreCompletedTransactionsFailedWithError:メソッドは、StoreKitがトランザクションの復元を終了するとエラーの有無に応じて呼び出されます。これらのメソッドの実装では、AppのUIが処理の成功またはエラーを反映するように更新します。

購入の持続

プロダクトの提供を開始した後、Appは購入の持続的な記録を作成する必要があります。Appは起動時にその持続的な記録を使用して、プロダクトの提供を継続します。また、購入の復元にも記録を使用します(「購入したプロダクトの復元」を参照)。Appを持続させる方法は、販売するプロダクトの種類とiOSのバージョンによって異なります。

User DefaultsシステムまたはiCloudを使用している場合、Appは数値やブール値などの値、またはトランザクションレシートのコピーを格納できます。macOSでは、defaultsコマンドを使用してUser Defaultsシステムを編集できます。レシートを格納するには追加のAppロジックが必要ですが、持続的な記録の改ざんを防止できます。

iCloudを使用して持続する場合、Appの持続的な記録はデバイス間で同期されますが、ほかのデバイスに関連コンテンツをダウンロードするのはAppです。

Appレシートを使用した持続

Appレシートには、ユーザーの購入記録とAppleによって暗号化された署名が含まれています。詳細については、『Receipt Validation Programming Guide』を参照してください。

消耗型プロダクトの情報は、支払いが行われるとシートに追加され、トランザクションを終了するまでレシート上に残ります。トランザクションの終了後、この情報はレシートが次に更新されるとき、たとえばユーザーが次に購入を行ったときに削除されます。

これ以外の種類のプロダクト購入の情報は、支払いが行われるとレシートに追加され、レシートに残り続けます。

User DefaultsまたはiCloudを使用した値の持続

User DefaultsまたはiCloudに情報を保存するには、キーの値を設定します。

#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
 
[storage setBool:YES forKey:@"enable_rocket_car"];
[storage setObject:@15 forKey:@"highest_unlocked_level"];
 
[storage synchronize];

User DefaultsまたはiCloudを使用したレシートの持続

User DefaultsまたはiCloudにトランザクションのレシートを保存するには、レシートのデータのキーの値を設定します。

#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
 
NSData *newReceipt = transaction.transactionReceipt;
NSArray *savedReceipts = [storage arrayForKey:@"receipts"];
if (!savedReceipts) {
    // 最初のレシートの保存
    [storage setObject:@[newReceipt] forKey:@"receipts"];
} else {
    // ほかのレシートの追加
    NSArray *updatedReceipts = [savedReceipts arrayByAddingObject:newReceipt];
    [storage setObject:updatedReceipts forKey:@"receipts"];
}
 
[storage synchronize];

独自のサーバを使用した持続

どのレシートがどのユーザーに属しているのかを追跡できるように、ある種の証明書またはIDとともにレシートのコピーをサーバに送信します。たとえば、電子メールまたはユーザー名とパスワードを使って、サーバに対してユーザーが本人であることを証明します。UIDeviceidentifierForVendorプロパティは使用しないでください。デバイスが異なると、このプロパティの値も異なるため、同じユーザーの別のデバイスで行われた購入の識別と復元ができないので、このプロパティを使用することはできません。

Appの機能のアンロック

プロダクトによってAppの機能を有効にする場合、コードパスを有効にするためのブール値を設定し、必要に応じてユーザーインターフェイスを更新します。案ロックする機能を決定するには、トランザクション発生時にAppによって作成された持続的な記録を確認してください。購入の完了時とAppの起動時、Appはこのブール値を更新する必要があります。

たとえば、Appのレシートを使用する場合、コードは次のようになります。

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
 
// レシートを使用するためのカスタムメソッド
BOOL rocketCarEnabled = [self receipt:receiptData
        includesProductID:@"com.example.rocketCar"];

また、User Defaultsシステムを使用する場合は次のようになります。

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"];

その後、その情報を使用して、Appで適切なコードパスを有効にします。

if (rocketCarEnabled) {
    // ロケットカーを使用
} else {
    // 普通の自動車を使用
}

関連コンテンツの配信

プロダクトに関連付けられたコンテンツがある場合は、そのコンテンツもAppでユーザーに配信する必要があります。たとえば、ゲームのレベルを購入するときにレベルを定義したファイルを配信する必要がある場合、また音楽Appで追加の楽器を購入するときに、ユーザーがその楽器を演奏するために必要なサウンドファイルを配信する必要がある場合があります。

そのコンテンツをAppバンドルに埋め込むか、必要に応じてダウンロードできます。いずれの方法にもそれぞれの利点と欠点があります。Appバンドルに含めるコンテンツが少なすぎると、購入したものが小さなものであってもダウンロードされる間待つ必要があります。Appバンドルに多くを詰め込み過ぎると、Appの初期ダウンロード時間が長くなり、関連プロダクトの購入を望まないユーザーの空き容量を無駄にすることになります。また、Appのサイズが大きすぎると、携帯電話のネットワークではAppをダウンロードできなくなります。

特に、ユーザーの大半がプロダクトを購入することが予想される場合は、Appにはサイズの小さいファイル(数メガバイト程度まで)を埋め込みます。Appバンドルのコンテンツは、ユーザーが購入した時点ですぐに使用できます。ただし、Appバンドルのコンテンツの更新や追加を行う場合は、更新されたバージョンのAppを登録する必要があります。

必要に応じて、サイズの大きいファイルをダウンロードしてください。Appバンドルとコンテンツを分離しておくと、Appの初期ダウンロードが小さくて済みます。たとえば、ゲームの最初のレベルはAppにバンドルしておいて、残りのレベルはユーザーに購入してもらうことができます。Appバンドルにハードコーディングされているのではなく、Appが独自のサーバからプロダクトリストを取得する場合、コンテンツの追加やAppによってダウンロードされたコンテンツの更新のためにAppを再登録する必要はありません。

iOS 6以降では、ほとんどのAppにおいて、ダウンロード済みファイルにAppleによってホストされたコンテンツを使用する必要があります。Appleによってホストされたコンテンツバンドルを作成するには、XcodeでIn-App Purchase Contentターゲットを使用してApp Store Connectに登録します。Appleのサーバでコンテンツをホストする場合、サーバを提供する必要はありません。Appのコンテンツは、App Storeなどの大規模運用を支えているAppleのインフラストラクチャに保存されます。また、Appleによってホストされたコンテンツは、Appが実行されていない場合であっても、バックグラウンドで自動的にダウンロードされます。

独自のサーバインフラストラクチャがあり、旧バージョンのiOSをサポートする必要がある場合や、複数のプラットフォームでサーバインフラストラクチャを共有する場合、独自のコンテンツをホストすることも可能です。

ローカルコンテンツのロード

ロケットのロードには、Appバンドルから他のリソースをロードするときと同様に、NSBundleクラスを使用します。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"rocketCar"
                                     withExtension:@"plist"];
[self loadVehicleAtURL:url];

ホストしたコンテンツのAppleのサーバからのダウンロード

Appleによってホストされたコンテンツと関連付けられたプロダクトをユーザーが購入した場合、トランザクションキューのオブザーバに渡されたトランザクションにも関連コンテンツをダウンロードするSKDownloadのインスタンスが含まれます。

そのコンテンツをダウンロードするには、SKPaymentQueuestartDownloads:メソッドを呼び出すことによって、ダウンロードオブジェクトをトランザクションのdownloadsプロパティからトランザクションキューに追加します。downloadsプロパティの値がnilの場合は、このトランザクションにはAppleによってホストされたコンテンツがありません。Appのダウンロードとは異なり、コンテンツのダウンロードでは一定のサイズよりも大きなコンテンツであってもWi-Fi接続を自動的に要求しません。ユーザーからの明示的なアクションがない限り、大きなファイルのダウンロードでは携帯電話ネットワークの使用を避けてください。

進行状況をUIで更新するなど、ダウンロード状態の変更に応答するには、トランザクションキューのオブザーバにpaymentQueue:updatedDownloads:メソッドを実装します。ダウンロードが失敗した場合、errorプロパティの情報を使用してユーザーにエラーを通知します。

Appでエラーが正常に処理されることを確認します。たとえば、ダウンロード中にディスク容量が不足した場合、不完全なダウンロードを破棄するか、素ペースを確保してからダウンロードを再開するかをユーザーが選択できるようにします。

コンテンツのダウンロード状況をユーザーインターフェイスで更新するには、progressプロパティとtimeRemainingプロパティの値を使用します。ユーザーにダウンロード進行状況をコントロールしてもらうには、SKPaymentQueuepauseDownloads:メソッド、resumeDownloads:メソッド、およびcancelDownloads:メソッドを使用できます。ダウンロードが完了したかどうかを判別するには、downloadStateプロパティを使用します。状態をチェックするために、ダウンロードオブジェクトのprogressまたはtimeRemainingプロパティを使用しないでください。これらのプロパティはUIの更新用です。

iOSでは、ダウンロードしたファイルをAppで管理できます。StoreKitフレームワークによって保存されたファイルは、Caches辞書でバックアップフラグが設定されていない状態になっています。ダウンロード完了後は、Appが適切な場所に移動します。デバイスのディスク容量が不足した場合に削除可能なコンテンツ(後でAppによって再ダウンロード可能)については、ファイルをCachesディレクトリに残します。その他の場合、ファイルをDocumentsフォルダに移動、ユーザーのバックアップから除外するフラグを設定します。

リスト 5-3 ダウンロード済みコンテンツのバックアップからの除外

NSError *error;
BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
                              forKey:NSURLIsExcludedFromBackupKey
                               error:&error];
if (!success) { /* エラー処理... */ }

macOSでは、ダウンロードされたコンテンツはシステムによって管理されます。Appで直接これらのファイルを移動したり削除したりすることはできません。ダウンロード後にコンテンツのある位置を特定するには、ダウンロードオブジェクトのcontentURLプロパティを使用します。引き続いて起こる起動中にファイルの位置を特定するには、SKDownloadcontentURLForProductID:クラスメソッドを使用します。ファイルを削除するには、deleteContentForProductID:クラスメソッドを使用します。AppのレシートからプロダクトIDを読み込む方法の詳細については、『Receipt Validation Programming Guide』を参照してください。

独自サーバからのコンテンツのダウンロード

Appとサーバとの連携に関する詳細と、独自のサーバからコンテンツをダウンロードするプロセスの詳細およびメカニズムは、すべてユーザーに委ねられています。最小限、通信は次の手順を経ます。

  1. Appからサーバにレシートを送信し、コンテンツを要求します。

  2. Receipt Validation Programming Guide』の説明に従って、サーバはレシートを検証しプロダクトが購入済みであることを確立します。

  3. レシートが有効である場合は、サーバはAppに対してコンテンツで応答します。

Appでエラーが正常に処理されることを確認します。たとえば、ダウンロード中にディスク容量が不足した場合、不完全なダウンロードを破棄するか、素ペースを確保してからダウンロードを再開するかをユーザーが選択できるようにします。

コンテンツをホストする方法、およびAppがサーバと通信する方法に関しては、セキュリティを実装することを検討してください。詳細については、『Security Overview』を参照してください。

トランザクションの終了

トランザクションが終了すると、購入で必要なすべての処理が完了したことがStoreKitに通知されます。終了していないトランザクションは終了するまではキューに残されたままになります。Appが起動されるたびにAppによって未完のトランザクションを終了できるように、トランザクションキューのオブザーバが呼び出されます。Appは、処理結果(成功か否か)にかかわらず、トランザクションをすべて終了させる必要があります。

トランザクションを終了する前には、次のアクションをすべて完了してください。

トランザクションを終了するには、支払いキューでfinishTransaction:メソッドを呼び出します。

SKPaymentTransaction *transaction = <# The current payment #>;
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

トランザクションの終了後には、終了したトランザクションでアクションを実行したり、プロダクトを配信したりする動作を一切行わないでください。未完了の動作がある場合は、トランザクションを終了する準備がAppでできていないことになります。

推奨されるテスト手順

正しく実装されていることを検証するために、コードの各部分をテストします。

支払い要求のテスト

既にテスト済みの有効なプロダクトIDを使用してSKPaymentのインスタンスを作成します。ブレークポイントを設定して、支払い要求を調査します。トランザクションキューに支払い要求を追加し、ブレークポイントを設定してオブザーバのpaymentQueue:updatedTransactions:メソッドが呼び出されることを確認します。

テスト中は、コンテンツを提供せずに直ちにトランザクションを終了しても構いません。ただしテスト中であっても、終了していないトランザクションがキュー内に残っていると、後で行うテストと干渉するため、トランザクションの終了に失敗すると問題が発生することがあります。

オブザーバコードの検証

トランザクションオブザーバのSKPaymentTransactionObserverプロトコルの実装を確認します。AppのストアUIが現在表示されておらず、また最近購入を行なっていない場合であっても、オブザーバがトランザクションを処理できることを確認します。

コードでSKPaymentQueueaddTransactionObserver:メソッドが呼び出されている場所を探します。Appの起動時にこのメソッドが呼び出されていることを検証します。

成功したトランザクションのテスト

テスト用ユーザーアカウントでApp Storeにサインインして、Appで購入を行います。トランザクションキューのオブザーバのpaymentQueue:updatedTransactions:メソッドの実装にブレークポイントを設定し、トランザクションを調査して、その状態がSKPaymentTransactionStatePurchasedであることを確認します。

コードで購入を存続している場所にブレークポイントを設定し、購入が成功したときにこのコードが呼び出されることを確認します。User DefaultsまたはiCloudキー値ストアを調査して、正しい情報が記録されていることを確認します。

中断したトランザクションのテスト

トランザクションキューのオブザーバのpaymentQueue:updatedTransactions:メソッドにブレークポイントを設定し、プロダクトを配信するかどうかを制御できるようにします。続いて、テスト環境で通常どおり購入をし、ブレークポイントを使用してトランザクションを維一時的に無視します。たとえば、LLDBのthread returnコマンドを使用して、メソッドから直ちに復帰します。

終了してAppを再起動します。起動後すぐに、StoreKitがpaymentQueue:updatedTransactions:メソッドを再び呼び出します。このときはAppで正常に応答します。Appでプロダクトが正しく配信され、トランザクションが完了したことを確認します。

トランザクションが終了したことの確認

AppでfinishTransaction:メソッドが呼び出されている場所を特定します。このメソッドが呼び出される前に、トランザクションに関係するすべての作業が完了していることを確認し、成功か否かにかかわらず、このメソッドが呼び出されることを確認します。