アプリ内課金(IAP)を実装する


アプリ内課金(IAP)を実装する

このページでは、タスクベースのアプローチでIAP APIを実装する方法について説明します。また、IAP v2.0 APIリファレンスドキュメントの補足となるユースケースとコードスニペットも紹介しています。なお、このドキュメント内のコードスニペットの多くは、消費型アイテムIAPのサンプルアプリに含まれています。

com.amazon.device.iapパッケージについて

com.amazon.device.iapパッケージは、Amazonアプリストアでアプリ内課金(IAP)APIを使用するために必要なクラスとインターフェイスを提供しています。

このパッケージには、以下のインターフェイスとクラスが含まれています。

  • ResponseReceiver: Amazonアプリストアからのブロードキャストインテントを受信するクラス
  • PurchasingService: Amazonアプリストアを通じてリクエストを開始するクラス
  • PurchasingListenerPurchasingServiceによって開始されたリクエストに対する非同期レスポンスを受信するインターフェイスです。

以下の表に、PurchasingServiceのリクエストメソッドと、関連付けられたPurchasingListenerのレスポンスコールバックを示します。いずれも、IAP APIを実装した場合に最もよく使用するメソッド、コールバック、レスポンスオブジェクトです。

PurchasingServiceのメソッド PurchasingListenerのコールバック レスポンスオブジェクト
getUserData() onUserDataResponse() UserDataResponse
getPurchaseUpdates() onPurchaseUpdatesResponse() PurchaseUpdatesResponse
getProductData() onProductDataResponse() ProductDataResponse
purchase() onPurchaseResponse() PurchaseResponse
notifyFulfillment() なし なし

これらのクラスとメソッドの詳細については、IAP v 2.0 APIリファレンスドキュメントを参照してください。

ResponseReceiver

アプリ内課金(IAP)APIは、すべての処理を非同期に実行します。開発するアプリは、ResponseReceiverクラスを介してAmazonアプリストアからブロードキャストインテントを受信する必要があります。このクラスがアプリ内で直接使用されることはありませんが、アプリがインテントを受信するには、ResponseReceiver用のエントリをAndroidManifest.xmlファイルに追加する必要があります。次のコードは、IAP v2.0用のAndroidManifest.xmlファイルにResponseReceiverを追加する方法を示しています。

 <application>
  ...
  <receiver android:name = "com.amazon.device.iap.ResponseReceiver" >
    <intent-filter>
      <action android:name = "com.amazon.inapp.purchasing.NOTIFY"
              android:permission = "com.amazon.inapp.purchasing.Permission.NOTIFY" />
    </intent-filter>
  </receiver>
  ...
 </application>

PurchasingService

PurchasingServiceクラスは、さまざまなタイプの情報を取得し、アイテム購入を実行して、Amazonに購入済みアイテムの付与完了を通知します。PurchasingServiceは、以下のメソッドを実装しています。

  • registerListener(PurchasingListener purchasingListener)PurchasingServiceクラス内のほかのメソッドを呼び出す前に呼び出すメソッド。
  • getUserData(): 現在ログオンしているユーザーのアプリ固有IDとマーケットプレイスを取得するために呼び出すメソッド。たとえば、ユーザーがアカウントを切り替えて使用する場合や、複数のユーザーが同じ端末でアプリにアクセスしている場合は、このメソッドを呼び出すと、現在のユーザーアカウントのレシートを確実に取得することができます。
  • getPurchaseUpdates(boolean reset): すべての端末上でこれまでに生じたすべての定期購入型アイテムおよび非消費型アイテムの購入情報を取得するために呼び出すメソッド。消費型アイテムの購入情報は、購入を行ったデバイスからのみ取得できます。GetPurchaseUpdatesでは、課金が完了していない消費型アイテムおよびキャンセルされた消費型アイテム購入情報のみが取得されます。このメソッドから返されるPurchaseUpdatesResponseデータを保持しておき、更新する場合のみシステムに問い合わせることをお勧めします。レスポンスはページ分割されます。
  • getProductData(java.util.Set skus): アプリに表示するSKUセットのアイテムデータを取得するには、このメソッドを呼び出します。
  • purchase(java.lang.String sku): 特定のSKUの購入を開始するには、このメソッドを呼び出します。
  • notifyFulfillment(java.lang.String receiptId, FulfillmentResult fulfillmentResult): 指定されたreceiptIdFulfillmentResultを送信するために呼び出すメソッド。FulfillmentResultに指定できる値は、FULFILLEDまたはUNAVAILABLEです。

PurchasingListener

非同期コールバックを処理するには、PurchasingListenerインターフェイスを実装します。これらのコールバックはUIスレッドで呼び出されるので、UIスレッドで長時間実行されるタスクを処理しないようにしてください。PurchasingListenerのインスタンスは、以下のメソッドを実装する必要があります。

  • onUserDataResponse(UserDataResponse userDataResponse): getUserData()を呼び出した後に呼び出されるメソッド。現在ログオンしているユーザーのUserIdmarketplaceを識別します。
  • onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse)getPurchaseUpdates(boolean reset)を呼び出した後に呼び出されるメソッド。購入履歴を取得します。このメソッドから返されるPurchaseUpdatesResponseデータを保持しておき、更新する場合のみシステムに問い合わせることをお勧めします。
  • onProductDataResponse(ProductDataResponse productDataResponse)getProductDataRequest(java.util.Set skus)を呼び出した後に呼び出されるメソッド。アプリで販売したいSKUに関する情報を取得します。onPurchaseResponse()で有効なSKUを使用します。
  • onPurchaseResponse(PurchaseResponse purchaseResponse)purchase(String sku)を呼び出した後に呼び出されるメソッド。購入のステータスを判断するために使用します。

レスポンスオブジェクト

PurchasingServiceを通じて開始した呼び出しに対するレスポンスはすべてPurchasingListenerによって受信されます。レスポンスごとに1つのレスポンスオブジェクトが使用されます。

  • UserDataResponse: 現在ログオンしているユーザーに、アプリ固有のUserIdmarketplaceを提供します。
  • PurchaseUpdatesResponse: ページ分割されたレシートのリストを提供。レシートは順番に並んでいません。
  • ProductDataResponse: SKUをキーとするアイテムデータを提供。getUnavailableSkus()メソッドは、利用不可能なSKUがあればリストします。
  • PurchaseResponse: アプリ内で開始された購入のステータスを提供。PurchaseResponse.RequestStatusの結果がFAILEDになる理由には、単にユーザーが購入手続きをキャンセルしただけの場合もあることに注意してください。

IAP APIをアプリに組み込む

IAPの実装に使用するクラスについて理解できたところで、次はアプリにIAPを実装するためのコードを書いてみましょう。

この手順で示すコードは、SDKに付属する消費型アイテムIAPサンプルアプリに含まれています。

  1. コードの準備をします。プレースホルダーまたはスタブコードを使って、以下の場所で下記指定のメソッドを呼び出すよう、アプリをセットアップしてください。
    • registerListener()をonCreate()メソッド内で呼び出す。
    • getUserData()onResume()メソッド内で呼び出す。
    • getPurchaseUpdates()onResume()メソッド内で呼び出す。
    • getProductData()onResume()メソッド内で呼び出す。

      これら4つの呼び出しはPurchasingServiceクラスの一部で、アプリ内課金実行の基礎となります。これ以降の手順では、上記の呼び出しの実装方法について詳しく説明し、コードを書く際のモデルとなるサンプルコードを紹介します。

  2. コードにPurchasingListenerを実装・登録することで、ResponseReceiverによってトリガーされるコールバックをリッスン・処理できるようにします。前の手順で設定した呼び出しは次のようになります。

    private SampleIapManager sampleIapManager;
    protected void onCreate(final Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setupApplicationSpecificOnCreate();
      setupIAPOnCreate();
    }
    
    private void setupIAPOnCreate() {
      sampleIapManager = new SampleIapManager(this);
    
      final SamplePurchasingListener purchasingListener = new SamplePurchasingListener(sampleIapManager);
      Log.d(TAG, "onCreate: registering PurchasingListener");
    
      PurchasingService.registerListener(this.getApplicationContext(), purchasingListener);
    
      Log.d(TAG, "IS_SANDBOX_MODE:" + PurchasingService.IS_SANDBOX_MODE);
    }
    

    このサンプルコードは、PurchasingListenerを登録するだけでなく、以下の2つのオプションのタスクを実行します。

    • 新しいSampleIapManagerインスタンスを作成して、購入レシートに関連するデータを格納する。このタスクは省略できますが、その場合はアクセス可能なほかの場所に購入レシートデータを格納する必要があります。データベースを使用するかメモリ内にデータを格納するかは開発者が決定することになります。
    • PurchasingService.IS_SANDBOX_MODEをチェックして、アプリがサンドボックス(テスト)モードで動作しているかどうかを確認する。その結果を表すフラグは、アプリの開発中や、App Testerを使用してローカルにアプリをテストしているときに役立ちます。
  3. getUserData()onResume()に実装して、現在のユーザー(ユーザーIDとマーケットプレイス)を識別します。

    
    // ...
    
    private String currentUserId =  null ;
    private String currentMarketplace =  null ;
    
    // ...
    
    public void onUserDataResponse( final UserDataResponse response) {
    
     final UserDataResponse.RequestStatus status = response.getRequestStatus();
    
     switch (status) {
       case SUCCESSFUL:
    	 currentUserId = response.getUserData().getUserId();
    	 currentMarketplace = response.getUserData().getMarketplace();
    	 break ;
    
       case FAILED:
       case NOT_SUPPORTED:
    	 // Failを返します。
    	 break ;
     }
    }
    

    この例では、後で利用できるようにユーザーIDとマーケットプレイスもメモリ内に保持しています。

  4. onResume()呼び出し内でgetPurchaseUpdates()呼び出しを実装します。これにより、このメソッドが前回呼び出された後にユーザーが行った購入トランザクションをすべて取得することができます。
    • resetfalseに設定すると、前回getPurchaseUpdates()が呼び出された後の、購入履歴のレスポンスがページ分割されて返されます。この呼び出しで、ユーザーの保留中の消費型アイテム、非消費型アイテム、定期購入型アイテムの購入に関するレシートが取得されます。(基本的にこの手法を用いることを推奨します。)
    • ユーザーの購入履歴全体を取得して、サーバー側のデータキャッシュなどに保存したり、メモリ内にすべて保持したりする場合に限り、resettrueに設定します。

      resetフラグにどの値を指定する場合でも、getPurchaseUpdates()は以下のような動作をすることに注意してください。

      • getPurchaseUpdates()を呼び出すと、アイテム付与が終わっていない消費型アイテムの購入すべてが返される。
      • getPurchaseUpdates()でアイテム付与が終わっているアイテムの購入情報が返されることは、極めて例外的な環境でのみ起こり得る。たとえば、アイテム付与後にアプリがクラッシュしたにもかかわらずAmazonに通知されていない場合や、アイテム付与後にAmazon側で問題が発生した場合には、getPurchaseUpdates()はアイテム付与が済んでいる消費型アイテムの購入情報を返します。このような状況では、アイテムに対して二重に課金することを避けるために、重複しているレシートを無視する必要があります。アイテムを提供する際に、そのことをどこかに記録し、2つ目のレシートを受信しても二重に提供しないようにしてください。

        getPurchaseUpdates()から返されたレスポンスにより、PurchasingListener.onPurchaseUpdatesResponse()コールバックがトリガーされます。

        @Override
        protected void onResume() {
          super.onResume();
        
        //...
        
          PurchasingService.getUserData();
        
        //...
        
          PurchasingService.getPurchaseUpdates(false);
        }
        

        続いて、レスポンスの処理を行います。

        1. PurchasingListener.onPurchaseUpdatesResponse()コールバックがトリガーされたら、PurchaseUpdatesResponse.getPurchaseUpdatesRequestStatus()によって返されるリクエストステータスを確認します。requestStatusがSUCCESSFULの場合は、各レシートを処理します。
        2. ページ分割を処理するには、PurchaseUpdatesResponse.hasMore()の値を取得します。

          PurchaseUpdatesResponse.hasMore()trueを返す場合は、以下のサンプルコードに示すように、getPurchaseUpdates()の再帰呼び出しを行います。

          public class MyPurchasingListener  implements PurchasingListener {
             boolean reset =  false ;
             //...
          
             public void onPurchaseUpdatesResponse( final PurchaseUpdatesResponse response) {
               //...
          
               // レシートを処理する
               switch (response.getPurchaseUpdatesRequestStatus()) {
                 case SUCCESSFUL:
                   for ( final Receipt receipt : response.getReceipts()) {
                     // レシートを処理する
                   }
          
                   if (response.hasMore()) {
                     PurchasingService.getPurchaseUpdates(reset);
                   }
                   break ;
                 case FAILED:
                   break ;
               }
             }
          
             //...
          }
          
  5. onResume()メソッド内でも、getProductData()を呼び出してSKUを検証します。これは、無効なSKUが原因でユーザーの購入が失敗することを避けるためです。

    以下のサンプルコードでは、アプリの消費型アイテム、非消費型アイテム、定期購入型アイテムのSKUを検証しています。

    protected void onResume() {
       super.onResume();
    
       // ...
    
       final Set <string>productSkus =  new HashSet<string>();
       productSkus.add( "com.amazon.example.iap.consumable" );
       productSkus.add( "com.amazon.example.iap.entitlement" );
       productSkus.add( "com.amazon.example.iap.subscription" );
       PurchasingService.getProductData(productSkus);
    
       Log.v(TAG,  "Validating SKUs with Amazon" );
       }
    

    PurchasingService.getProductData()メソッドを呼び出すと、PurchasingListener.onProductDataResponse()コールバックが呼び出されます。ProductDataResponse.getRequestStatus()で返されるリクエストステータスを確認して、この呼び出しによって検証されたアイテムまたはSKUのみを販売します。

    • requestStatusSUCCESSFULの場合は、アプリに表示されるSKUで識別される商品データマップを取得します。商品データマップには、以下の値が含まれます。
      • Product Type(商品タイプ)
      • Icon URL(アイコンのURL)
      • Localized price(マーケットプレイス現地価格)
      • Title(タイトル)
      • Description(概要)
      • SKU(識別コード)

        アプリ内でIAPアイコンを表示する場合は、AndroidManifest.xmlファイルを編集してandroid.permission.INTERNET権限を含める必要があることに注意してください。

        さらに、requestStatusSUCCESSFULであっても、利用不可能なSKUがある場合は、PurchaseUpdatesResponse.getUnavailableSkus()を呼び出して、無効なSKUの商品データを取得し、アプリのユーザーがその商品を購入できないようにしてください。

    • requestStatusFAILEDの場合は、以下のサンプルコードに示すように、アプリ内のIAP機能を無効にします。

      public class MyPurchasingListener  implements PurchasingListener {
         //  ...
      
         public void onProductDataResponse( final ProductDataResponse response) {
           switch (response.getRequestStatus()) {
             case SUCCESSFUL:
               for ( final String s : response.getUnavailableSkus()) {
                 Log.v(TAG,  "Unavailable SKU:" + s);
               }
      
               final Map <string,>products = response.getProductData();
               for ( final String key : products.keySet()) {
                 Product product = products.get(key);
                 Log.v(TAG, String.format( "Product: %s\n Type: %s\n SKU: %s\n Price: %s\n Description: %s\n" , product.getTitle(), product.getProductType(), product.getSku(), product.getPrice(), product.getDescription()));
               }
               break ;
      
             case FAILED:
               Log.v(TAG,  "ProductDataRequestStatus:  FAILED" );
               break ;
           }
         }
      
         //  ...
      }
      
  6. 購入を実行するコードを作成します。この例では消費型アイテムの購入を処理しますが、定期購入型アイテムと非消費型アイテムにも同じコードを使用できます。

    MainActivityに含まれる次のコードは、PurchasingService.purchase()を呼び出して購入を初期化します。消費型アイテムのサンプルアプリでは、アプリユーザーが [Buy Orange] ボタンをタップすると、このメソッドが実行されます。

      public void onBuyOrangeClick(final View view) {
         final RequestId requestId = PurchasingService.purchase(MySku.ORANGE.getSku());
         Log.d(TAG, "onBuyOrangeClick: requestId (" + requestId + ")");
      }
    

    次に、SamplePurchasingListener.onPurchaseResponse()コールバックを実装します。以下のコードでは、SampleIapManager.handleReceipt()によって実際の購入が処理されます。

    public void onPurchaseResponse(final PurchaseResponse response) {
    	switch (status) {
    		// ...
    		case SUCCESSFUL:
    			final Receipt receipt = response.getReceipt();
    			iapManager.setAmazonUserId(response.getUserData().getUserId(), response.getUserData().getMarketplace());
    			Log.d(TAG, "onPurchaseResponse: receipt json:" + receipt.toJSON());
    			iapManager.handleReceipt(receipt, response.getUserData());
    			iapManager.refreshOranges();
    			break;
    	}
    }
    
  7. 購入アイテムの付与を完了し、購入レシートを処理します。自分でアプリを設計する際は、この手順をすべて1つの場所で処理するため、アイテム付与を実行する仕組みを実装することになる可能性がある点に注意してください。
    • 購入可能アイテムが定期購入である場合は、receiptIdの値について以下の点にご注意ください。

      • 定期購入が継続されていて、一度も途中でキャンセルされていない場合、アプリはその定期購入型アイテム/ユーザーに関するレシートを1回のみ受け取る。

      • 定期購入が継続されていない場合、たとえばユーザーが自動更新を選択しておらず、定期購入の期限が切れた1か月後に再び定期購入を開始した場合は、アプリは複数のレシートを受け取る。

    • アイテム付与の前に、バックエンドサーバーでAmazonのレシート検証サービス(RVS)を使用してreceiptIdを検証することにより、購入のレシートを検証します。Amazonは、RVSのサンドボックス環境とRVSの本番環境の両方を提供しています。RVSを使用できるようにRVS Sandboxおよびサーバーをセットアップする方法については、レシート検証サービス(RVS)のドキュメントを参照してください。
      • 開発中は、RVSのサンドボックス環境を使用してApp Testerが生成したレシートを検証してください。
      • 本番環境では、RVS本番環境エンドポイントを使用してください。
    • この例では、SampleIapManagerで、レシートがキャンセルされているかどうかをhandleConsumablePurchase()メソッドが確認します。

      • レシートがキャンセルされたものの、アイテムの付与が完了していた場合には、revokeConsumablePurchase()メソッドを呼び出して購入を取り消します。
      • レシートがキャンセルされていない場合は、RVSを使用してサーバーからレシートを検証し、grantConsumablePurchase()を呼び出して購入のアイテム付与を行います。

        public void handleConsumablePurchase(final Receipt receipt, final UserData userData) {
            try {
                if (receipt.isCanceled()) {
                    revokeConsumablePurchase(receipt, userData);
                } else {
                    // サーバー側でレシートを検証することを強くお勧めします
                    if (!verifyReceiptFromYourService(receipt.getReceiptId(), userData)) {
                        // 購入を確認できない場合は、
                        // 適切なエラーメッセージをユーザーに表示します。
                        mainActivity.showMessage("Purchase cannot be verified, please retry later.");
                        return;
                    }
                    if (receiptAlreadyFulfilled(receipt.getReceiptId(), userData)) {
                        // レシート内容のアイテムが以前に付与済みである場合は、付与済みであることを
                        // Amazonアプリストアに再度通知します。
                        PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
                        return;
                    }
        
                    grantConsumablePurchase(receipt, userData);
                }
                return;
            } catch (final Throwable e) {
                mainActivity.showMessage("Purchase cannot be completed, please retry");
            }
        }
        
  8. 以下のタスクを実行して、アイテムをユーザーに付与します。

    1. 対象の購入に関する購入記録を作成し、その記録をどこかに格納します。
    2. SKUを検証します。

      • SKUが利用可能な場合、アイテムの付与を行い、notifyFulfillmentをステータスFULFILLEDで呼び出す。この手順が完了すると、Amazonアプリストアはそれ以上アプリにこの購入のレシートを送信しなくなります。
      • SKUを利用できない場合は、notifyFulfillmentをステータスUNAVAILABLEで呼び出す。

        private void grantConsumablePurchase(final Receipt receipt, final UserData userData) {
            try {
                // 下記サンプルコードは簡易な実装方法です。独自の付与ロジックを
                // 実装する際は、スレッドセーフ、トランザクション性、堅牢性に
                // 注意してください。
        
                // アプリ/サーバーで購入情報を作成し、
                // ユーザーに購入したアイテムを付与します。このサンプルの場合は、ユーザーにオレンジを1つ
                // 付与します。
                createPurchase(receipt.getReceiptId(), userData.getUserId());
                final MySku mySku = MySku.fromSku(receipt.getSku(), userIapData.getAmazonMarketplace());
                // SKUがまだ利用可能であることを検証します。
                if (mySku == null) {
                    Log.w(TAG, "The SKU [" + receipt.getSku() + "] in the receipt is not valid anymore ");
                    // SKUが利用できなくなった場合は、
                    // PurchasingService.notifyFulfillmentをステータス"UNAVAILABLE"で呼び出します。
                    updatePurchaseStatus(receipt.getReceiptId(), null, PurchaseStatus.UNAVAILABLE);
                    PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.UNAVAILABLE);
                    return;
                }
        
                if (updatePurchaseStatus(receipt.getReceiptId(), PurchaseStatus.PAID, PurchaseStatus.FULFILLED)) {
                    // SQLiteデータベースの購入ステータスを「成功」に更新する
                    userIapData.setRemainingOranges(userIapData.getRemainingOranges() + 1);
                    saveUserIapData();
                    Log.i(TAG, "Successfuly update purchase from PAID->FULFILLED for receipt id " + receipt.getReceiptId());
                    // Amazonアプリストアにステータスのアップデートを渡す。購入のステータスとしてFulfilledを
                    // 受け取ると、Amazonはこれ以上購入レシートを
                    // アプリに送信しなくなります。
                    PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
                } else {
                    // SQLiteデータベースの購入ステータスの更新に失敗した - ステータスが
                    // 既に変更されていた。
                    // これは通常、同じレシートが別のonPurchaseResponse
                    // またはonPurchaseUpdatesResponseコールバックで更新されていたことを意味します。
                    // このサンプルコードでは、エラーを表示せずにログ記録のみを行います。
                    Log.w(TAG, "Failed to update purchase from PAID->FULFILLED for receipt id " + receipt.getReceiptId()
                               + ", Status already changed.");
                }
        
            } catch (final Throwable e) {
                // 何らかの理由でアプリが購入アイテムを付与できない場合のために、
                // ここに独自のエラー処理コードを追加してください。
                // 次にPurchasingService.getPurchaseUpdates APIが呼び出されたときに
                // Amazonから消費型アイテムの購入レシートが再度送信されます。
                Log.e(TAG, "Failed to grant consumable purchase, with error " + e.getMessage());
            }
        }
        
  9. レシートを処理します。この手順は、getPurchaseUpdates()の結果であり、purchase(SKU)を呼び出した結果ではない点に注意してください。SampleIapManager.handleReceiptメソッドを呼び出して、PurchaseUpdatesResponseの一部として返されるすべてのレシートを処理します。

    public void onPurchaseUpdatesResponse(final PurchaseUpdatesResponse response) {
    // ....
    switch (status) {
    case SUCCESSFUL:
    	iapManager.setAmazonUserId(response.getUserData().getUserId(), response.getUserData().getMarketplace());
    	for (final Receipt receipt : response.getReceipts()) {
    		iapManager.handleReceipt(receipt, response.getUserData());
    	}
    	if (response.hasMore()) {
    		PurchasingService.getPurchaseUpdates(false);
    	}
    	iapManager.refreshOranges();
    	break;
    // ...