アプリ内課金(IAP)APIを組み込む方法
このページでは、Androidアプリにアプリ内課金(IAP)APIを組み込む方法について説明し、IAP v2.0 APIリファレンスドキュメント(英語のみ)の補足となるユースケースとコードスニペットを紹介します。このドキュメントに記載されているコードスニペットの多くは、消費型アイテムのIAPサンプルアプリで使用されています。
IAPに対応したAndroidアプリの作成に必要な手順は、IAP eBook(英語のみ)に詳しく記載されています。
Android IAPパッケージについて
com.amazon.device.iap
パッケージには、AndroidアプリにIAPを実装するためのクラスとインターフェイスが用意されています。
このパッケージには、次のインターフェイスとクラスが含まれています。
- ResponseReceiver: Amazonアプリストアからのブロードキャストインテントを受信するクラス
- PurchasingService: Amazonアプリストアを通じてリクエストを開始するクラス
- PurchasingListener:
PurchasingService
によって開始されたリクエストに対する非同期レスポンスを受け取るインターフェイス
以下の表に、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アプリストアからブロードキャストインテントを受信する必要があります。このクラスがアプリ内で直接使用されることはありませんが、アプリでインテントを受信するには、AndroidManifest.xml
ファイルにResponseReceiver
のエントリを追加する必要があります。次のコードスニペットは、IAP v2.0用のAndroidManifest.xmlファイルにResponseReceiverを追加する方法を示しています。
<application>
...
<receiver android:name = "com.amazon.device.iap.ResponseReceiver"
android:permission = "com.amazon.inapp.purchasing.Permission.NOTIFY" >
<intent-filter>
<action android:name = "com.amazon.inapp.purchasing.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)
: 指定したreceiptId
のFulfillmentResult
を送信するには、このメソッドを呼び出します。FulfillmentResult
に指定できる値は、FULFILLEDまたはUNAVAILABLEです。
PurchasingListener
非同期コールバックを処理するには、PurchasingListener
インターフェイスを実装します。これらのコールバックはUIスレッドで呼び出されるため、長時間実行されるタスクをUIスレッドで処理しないようにしてください。PurchasingListener
のインスタンスには、次のメソッドを実装する必要があります。
onUserDataResponse(UserDataResponse userDataResponse)
: getUserData()の呼び出し後に呼び出されます。現在ログオンしているユーザーのUserId
とmarketplace
を特定します。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
: 現在ログオンしているユーザーのアプリ固有のUserId
とmarketplace
を提供します。PurchaseUpdatesResponse
: ページ分割されたレシートリストを提供します。レシートはソートされていません。ProductDataResponse
: SKUをキーとするアイテムデータを提供します。アイテムデータを入手できないSKUのリストはgetUnavailableSkus()
メソッドで取得できます。PurchaseResponse
: アプリ内で開始された購入のステータスを提供します。PurchaseResponse.RequestStatus
の示す結果がFAILEDになる理由には、単にユーザーが購入手続きをキャンセルしただけの場合もあることに注意してください。
IAP APIをアプリに組み込む方法
ここまで、IAPの実装に必要なクラスについて少し詳しく説明してきました。次は、アプリにIAPのコードを記述していきます。
このセクションのコードスニペットは、SDKに付属する消費型アイテムのIAPサンプルアプリから引用したものです。
1.プレースホルダーメソッドの作成
コードの骨組みを作るために、以下の場所で以下のメソッドを呼び出すプレースホルダーつまりスタブコードを作成します。
onCreate()
メソッド内でregisterListener
()を呼び出す。onResume()
メソッド内でgetUserData()
を呼び出す。onResume()
メソッド内でgetPurchaseUpdates()
を呼び出す。onResume()
メソッド内でgetProductData()
を呼び出す。
これら4つの呼び出しはPurchasingService
クラスの一部であり、IAPを実行するための基盤となります。以降の手順では、上記の呼び出しの実装方法について詳しく説明し、独自のコードを記述するときにモデルとして使用できるサンプルコードを紹介します。
2.PurchasingListenerの実装と登録
ResponseReceiver
によってトリガーされるコールバックをアプリでリッスンして処理できるように、コードにPurchasingListener
を実装して登録します。前の手順で設定した呼び出しは次のようになります。
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: 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.ユーザー情報の取得
onResume()
でgetUserData()
を呼び出して、現在のユーザーに関する情報(ユーザー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:
// 失敗時の処理を適切に行います。
break ;
}
}
この例では、後で使用できるようにユーザーIDとマーケットプレイスをメモリ内に保持しています。
4. getPurchaseUpdatesメソッドの実装
onResume()
にgetPurchaseUpdates()
の呼び出しを実装します。このメソッドでは、前回の呼び出し以降にユーザーが行った購入トランザクションをすべて取得できます。
reset
をfalse
に設定すると、レスポンスとして、前回getPurchaseUpdates()
が呼び出されてからの購入履歴がページ分割されて返されます。この呼び出しで、ユーザーの保留中の消費型アイテム、非消費型アイテム、定期購入型アイテムの購入に関するレシートが取得されます(Amazonでは、基本的にこのアプローチを使用することを推奨します)。- ユーザーの購入履歴全体を取得して、サーバー側のデータキャッシュなどに保存したり、メモリ内にすべてを保持したりする場合に限り、
reset
をtrue
に設定します。
reset
フラグの値にかかわらず、getPurchaseUpdates()
は次のように動作する点に注意してください。
getPurchaseUpdates()
呼び出しは、付与が完了していないすべての消費型アイテムの購入情報を常に返します。- ごくまれに、アイテム付与が完了している購入情報が
getPurchaseUpdates()
の呼び出しから返されることがあります。たとえば、アイテムを付与した後、Amazonに通知する前にアプリがクラッシュした場合や、アイテムの付与後にAmazon側で問題が発生した場合は、付与が完了している消費型アイテムの購入情報がgetPurchaseUpdates()
から返されます。このような状況では、アイテムを二重に付与することを避けるために、重複しているレシートを無視する必要があります。アイテムの配信時は、何らかの方法で配信状況を記録しておき、2つ目のレシートを受け取っても二重に配信しないようにしてください。
getPurchaseUpdates()
からレスポンスが返されると、PurchasingListener.onPurchaseUpdatesResponse()
コールバックがトリガーされます。
@Override
protected void onResume() {
super.onResume();
//...
PurchasingService.getUserData();
//...
PurchasingService.getPurchaseUpdates(false);
}
続いて、レスポンスの処理を行います。
PurchasingListener.onPurchaseUpdatesResponse()
コールバックがトリガーされたら、PurchaseUpdatesResponse.getPurchaseUpdatesRequestStatus()
から返されるリクエストステータスを確認します。requestStatus
がSUCCESSFULの場合は、各レシートを処理します。
ページ分割を処理するには、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.getProductDataメソッドの実装
同じくonResume()
メソッド内で、getProductData()
を呼び出してSKUを検証します。これは、無効なSKUが原因でユーザーの購入が失敗することを避けるためです。
次のサンプルコードでは、アプリの消費型アイテム、非消費型アイテム、定期購入型アイテムのSKUをAmazonで検証します。
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, "SKUをAmazonで検証します" );
}
productSkus
に親SKUとすべての子SKUを追加して、getProductData
で検証します。子SKUを含める必要があるのは、価格情報が子SKUに関連付けられているためです。価格は定期購入期間によって異なるので、親SKUには価格がありません。詳細については、定期購入型アイテムに関する質問を参照してください。PurchasingService.getProductData()
メソッドを呼び出すと、PurchasingListener.onProductDataResponse()
コールバックが呼び出されます。ProductDataResponse.getRequestStatus()
から返されたリクエストステータスを確認し、この呼び出しによって検証されたアイテムまたはSKUのみを販売します。
成功したリクエスト
requestStatus
がSUCCESSFUL
の場合は、アプリに表示するSKUをキーとして商品データマップを取得します。商品データマップには次の値が含まれます。
- 商品タイプ
- アイコンのURL
- ローカライズされた価格(定期購入型アイテムの子SKUの場合は子SKUに関連付けられている)
- タイトル
- 説明
- SKU
アプリ内でIAPアイコンを表示する場合は、AndroidManifest.xml
ファイルを編集して、android.permission.INTERNET
パーミッションを含める必要があります。
また、requestStatus
がSUCCESSFUL
であっても、アイテムデータを入手できないSKUが存在する場合は、PurchaseUpdatesResponse.getUnavailableSkus()
を呼び出して無効なSKUのリストを取得し、アプリのユーザーがそれらの商品を購入できないようにしてください。
失敗したリクエスト
requestStatus
がFAILED
の場合は、次のサンプルコードに示すように、アプリの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, "アイテムデータを入手できないSKU:" + s);
}
final Map <string,>products = response.getProductData();
for ( final String key : products.keySet()) {
Product product = products.get(key);
Log.v(TAG, String.format( "商品:%s\n タイプ:%s\n SKU:%s\n 価格:%s\n 説明:%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 Sandbox環境とRVS本番環境の両方を提供しています。RVS SandboxとサーバーをセットアップしてRVSを使用できるようにする方法については、レシート検証サービス(RVS)の ドキュメントを参照してください。* 開発中は、RVS Sandbox環境を使用して、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("購入を検証できませんでした。後でもう一度お試しください。"); return; } if (receiptAlreadyFulfilled(receipt.getReceiptId(), userData)) { // レシートのアイテムが以前に付与されている場合は、付与済みであることを // Amazonアプリストアに改めて通知します。 PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED); return; } grantConsumablePurchase(receipt, userData); } return; } catch (final Throwable e) { mainActivity.showMessage("購入を完了できませんでした。もう一度お試しください。"); } }
8.ユーザーへのアイテムの付与
アイテムをユーザーに付与するには、購入レコードを作成し、そのレコードを永続的な場所に保存します。
さらに、SKUを検証します。
- アイテムデータを入手できるSKUの場合は、アイテムを付与し、ステータスFULFILLEDを指定して
notifyFulfillment
を呼び出します。この手順が完了すると、その購入レシートがAmazonアプリストアからアプリに送信されることはなくなります。 - アイテムデータを入手できないSKUの場合は、ステータスUNAVAILABLEを指定して
notifyFulfillment
を呼び出します。
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, "レシート内のSKU [" + receipt.getSku() + "] は無効になっています。");
// SKUが適用不可になっている場合は、ステータス「UNAVAILABLE」を指定して
// PurchasingService.notifyFulfillmentを呼び出します。
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, "購入ステータスをPAIDからFULFILLEDに正しく更新しました。レシートID:" + receipt.getReceiptId());
// Amazonアプリストアにステータスの更新を渡します。購入に対して付与完了の
// ステータスが通知されると、Amazonはその購入レシートをアプリに送信しなく
// なります。
PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
} else {
// SQLiteデータベースの購入ステータスの更新に失敗 - ステータスは
// 既に変更されています。
// これは通常、同じレシートが別のonPurchaseResponseまたは
// onPurchaseUpdatesResponseコールバックで更新されたことを意味します。
// このサンプルコードでは、エラーを表示せずにログのみを出力します。
Log.w(TAG, "購入ステータスをPAIDからFULFILLEDに更新できませんでした。レシートID:" + receipt.getReceiptId()
+ "。ステータスは既に変更されています。");
}
} catch (final Throwable e) {
// 何らかの理由でアプリが購入アイテムを付与できない場合のために、
// ここに独自のエラー処理コードを追加します。
// 次にPurchasingService.getPurchaseUpdates APIを呼び出すと、
// Amazonから消費型アイテムの購入レシートが再度送信されます。
Log.e(TAG, "購入された消費型アイテムを付与できませんでした。エラー:" + 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;
// ...