Google Basic
Overview
Androidアプリ内課金の基本的な実装手法ついてまとめているセクション。
アイテム購入時にアプリが実行するアプリ内課金は全部で5種類ある。
レスポンスも同様に5種類あり、アプリ内課金APIの戻り値として返ってくる。
非同期で返ってくる場合もあるが、その場合はイベントリスナーで結果を取得する必要がある
課金アイテム取得メソッドの実装における注意点
getSkuDetails() はかつて商品情報を取得する標準的なメソッドとして利用されていたが、同期的APIであるためUIスレッドで呼ぶと画面が固まる(ブロッキング)という重大な問題を持っていた。
実際には、Playストアアプリとの通信のためにネットワーク越しの処理が走るため、数百ms〜数秒かかる場合がある。このため、Billing Library v2以前では、開発者が明示的に別スレッド(Thread / AsyncTask / ExecutorService など)で呼び出す必要があり、「getSkuDetailsはUIスレッドで呼んではいけない」という注意がドキュメントでも頻出していた。
// v1〜v2 時代の典型的な安全実装例(Java)
new Thread(() -> {
SkuDetailsParams params = ...;
SkuDetailsResult result = billingClient.querySkuDetails(params);
runOnUiThread(() -> {
// UI更新処理
});
}).start();
現在の実装:非同期メソッドへの移行
Billing Library v5以降では、queryProductDetailsAsync() という非同期APIが導入され、内部で自動的にスレッド制御されるようになった。これにより、開発者が手動でスレッドを切り替える必要はなくなり、UIスレッドブロッキングのリスクも解消された。
// v5以降の推奨実装(Kotlin)
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
// 商品情報の表示やUI更新処理など
}
現在では getSkuDetails() は非推奨となっており、必ず queryProductDetailsAsync() を使用すべきである。
5種類のアプリ内課金API
1. isBillingSupported メソッド
Playストアアプリがアプリ内課金をサポートしていることを確認するためのリクエストを送信する。
例えばアプリ起動時にこのリクエストを送信してPlayストアアプリがアプリ内課金をサポートしていない場合は、アイテムの購入ボタンに遷移するためのボタンを押せないようにする場合などに使用する
詳細解説(isBillingSupported)
このメソッドは、Google Play Billing ライブラリ(旧AIDL含む)で提供されていたもので、端末が指定された課金タイプ(inapp、subs など)に対応しているかどうかをチェックする目的で使用される。
現在でも isBillingSupported は一部のケースでは有効だが、Billing Library v3以降では主に BillingClient による接続チェック(startConnection や isReady())が使われるようになっており、本メソッドはレガシーな位置づけになりつつある。
使用できない/正しく機能しない場合の例としては以下がある:
- Playストアアプリがインストールされていない/無効化されている端末(例:一部中華端末、AOSP端末)
- Google Play開発者サービスが古すぎる、または権限不足
- シミュレーターや非対応端末
- 指定された billingType に端末が未対応な場合(たとえば
subs非対応端末にサブスクリプションチェックを送った場合)
RevenueCat のような外部の課金ラッパーサービスを使っている場合、このメソッドを開発者が明示的に使う必要はない。
RevenueCat SDK 内部で BillingClient をラップしており、自動的に課金対応状況や接続状態をチェックしてくれるため、アプリ開発者がこのAPIを直接扱う必要は基本的にない。
「AOSP端末」とは、Googleの公式サービス(Google PlayストアやGoogleアカウントなど)が搭載されていないAndroid端末のこと。
AOSPは Android Open Source Project の略で、Googleが公開しているオープンソース版のAndroid OSを指す。
2. getPurchases メソッド
利用者の購入情報を取得するためのリクエストを送信する。
リクエストが成功するとPlayストアから購入情報を含んだBundleが返ってくるためその情報を基にアプリ内のアイテムを復元できる。
詳細解説(getPurchases)
getPurchases メソッドは、ユーザーがすでに購入したアイテム(非消費型や有効な定期購入など)を取得するために使用されるAPIである。
通常、アプリ起動時や復元処理時に呼び出し、購入済みのアイテムをアプリ内で有効化する目的で使われる。
現在では、Billing Library v5 以降においては、queryPurchasesAsync() メソッドが推奨されており、getPurchases() は非推奨(deprecated)になっている。
queryPurchasesAsync() は非同期であり、コールバックで結果を取得するため、UIスレッドブロッキングの問題が解消されている。
getPurchases の使用上の注意点:
- リアルタイム性が低い:
getPurchasesは Play ストアのキャッシュを返すため、最新状態を必ずしも反映しているとは限らない。 - トランザクション更新を検知できない:新しい購入・キャンセル・返金・更新などの情報は含まれない可能性がある。
- 非同期対応が弱い:古い実装ではメインスレッドでブロッキングしてしまうことがある。
よって、現在のベストプラクティスでは queryPurchasesAsync() を使い、必要に応じて PurchasesUpdatedListener や RevenueCat の Purchase SDK を併用してリアルタイムな購入検知と復元を行うことが推奨される。
queryPurchasesAsync() は「非同期メソッド」であり、呼び出し元の処理を止めずにバックグラウンドで課金情報の取得を行い、処理が完了したタイミングでコールバック関数が呼ばれる。
コールバックとは、ある処理(この場合は課金情報取得)が終了した後に呼び出される関数のことであり、以下のような形で記述される
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
) { billingResult, purchasesList ->
// 結果が返ってきた後にここが実行される
// UIの更新やアイテムの復元処理などを記述
}
一方で、古い同期的なAPI(getPurchases() など)では処理の完了までUIスレッド(メインスレッド)がブロックされる
UIスレッドとは、ユーザーのタップや画面描画を処理するスレッドであり、ここが止まると画面が固まったように感じられる。
これを「UIスレッドブロッキング」と呼び、UXに悪影響を与える原因となる。
そのため現在のベストプラクティスでは、非同期処理とコールバックを前提とした queryPurchasesAsync() の利用が推奨されている。
RevenueCatを使っている場合:
RevenueCatでは内部的にGoogle Play Billing APIを使用して購入情報の同期を行っており、アプリ側が getPurchases を直接呼び出す必要はない。
RevenueCat SDKが自動的にユーザーのアクティブな購入情報をバックエンドと同期し、アプリに購読状況を提供してくれるため、開発者の実装負荷が大きく軽減される。
3. getSkuDetails メソッド
アイテムの詳細情報を取得するためのメソッド。
アイテムの決済画面でアイテムの概要や価格の情報が表示されるが決済画面に線しないと価格がわからないというのは使い勝手が悪い。
そのためアプリは販売アイテム一覧画面などでアイテムの最新情報を表示するのが望ましい。
詳細解説(getSkuDetails)
getSkuDetails メソッドは、アプリ内課金で提供する商品(SKU:Stock Keeping Unit)の価格、タイトル、説明、通貨、定期購入の期間などの詳細情報を取得するために使用される。
この情報は、ユーザーに購入アイテムを表示する画面(商品リストUI)などに利用され、ローカライズされた価格や商品名が含まれているため、ハードコードせずにこのAPIから取得することが推奨されている。
現在の状況(非推奨)
getSkuDetails() は Billing Library v3 以前のAPIであり、現在では 非推奨(deprecated) となっている。
代わりに、Billing Library v5 以降では queryProductDetailsAsync() が使用される。
val params = QueryProductDetailsParams.newBuilder()
.setProductList(
listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("example_id")
.setProductType(BillingClient.ProductType.INAPP)
.build()
)
)
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
// productDetailsList に価格や説明などの詳細が含まれる
}
注意点
- 商品情報は Play Console 側で定義した内容が返ってくるため、サーバー側での価格管理はできない
- ネットワーク接続が必要であり、取得に数百ms〜数秒かかる場合があるため、非同期前提でUI設計が必要
- SKUは Product ID に名称変更されているが、意味はほぼ同じ(SKU = 商品ID)
RevenueCatの場合
RevenueCatを利用している場合、アプリ内で getSkuDetails や queryProductDetailsAsync() を明示的に呼び出す必要はない。
RevenueCatのバックエンドで商品情報を一元管理しており、アプリ側では RevenueCat SDK を通して Offering 情報として取得できる。これにより、複数プラットフォーム間での価格・商品名・説明などの整合性を保ちやすくなる。
4. getBuyIntent メソッド
アイテムの購入処理をするメソッド。
developerPayloadに指定される文字列は購入処理が完了するとGoogle Playから返却される。
アプリは購入処理の開始時に指定したdeveloperPayloadと返送されたdeveloperPayloadを比較することで購入処理の正当性を検証することができる。
developerPayload による購入正当性の検証(旧API)
getBuyIntent() を利用していた時代には、セキュリティ対策として developerPayload という任意の文字列をリクエストに含めることができた。
Google Play は購入処理の応答時に、この developerPayload を含めて返却するため、アプリ側で「送信した文字列と一致するか」を比較することで購入の正当性を検証するという方法が広く使われていた。
これは、アプリ内で購入情報を偽装されるリスク(リプレイ攻撃や改ざん)を軽減する意図があった。
String sentPayload = UUID.randomUUID().toString();
Bundle bundle = billingService.getBuyIntent(..., sentPayload);
// 購入応答に含まれる payload を検証
現在の状況(BillingClient / launchBillingFlow)
Billing Library v3 以降では developerPayload は 完全に非推奨(廃止) となっており、代わりに Google Play Billing が提供する署名付き購入トークンとバックエンド検証が推奨されている。
購入結果は PurchasesUpdatedListener 経由で通知され、その中の Purchase オブジェクトには purchaseToken と signature が含まれる。
検証方法は以下の通り
- アプリ側で
purchaseTokenを受け取る - 自社サーバー経由で Google Play Developer API に問い合わせて購入の有効性を検証する
- レスポンスとして返ってくるステータス(例:有効/返金済み/キャンセル済みなど)を確認する
上記は「初回購入時(launchBillingFlowの直後)」における検証フローを想定しているが、他にも検証が必要となるタイミングは複数存在する。
その他のタイミングにおける正当性確認
アプリ再起動時・復元時
- アプリが再起動した場合やユーザーがアプリを再インストールした場合でも、購入済みアイテムは復元される必要がある。
- このとき
PurchasesUpdatedListenerは呼ばれないため、queryPurchasesAsync()やBillingClient.queryPurchaseHistoryAsync()を用いて過去の購入履歴を取得し、purchaseTokenをサーバーで再検証することが求められる。
定期購入の継続検証
- サブスクリプションが自動更新された場合も、アプリ側に通知が飛ぶとは限らない(特にアプリを開いていない間)。
- そのためサーバー側で定期的に Google Play Developer API を用いて
purchaseTokenのステータス(有効・停止・キャンセル等)をチェックするのが推奨されている。
検証対象は常に purchaseToken
purchaseTokenは一意な購入トランザクションを示す識別子であり、これをもとにサーバーサイドで正当性を確認する設計が前提となっている。- アプリ側で署名検証も可能だが、セキュリティを担保するには Google API 経由での確認がベストプラクティス。
なお、RevenueCat を利用している場合はこれらの再検証やトークン管理も自動的に処理されるため、明示的にこの検証ロジックを実装する必要はない。
なぜ developerPayload が廃止されたか?
- developerPayload はアプリ内で検証されるだけであり、端末改ざんや偽装に弱い
- 一貫したセキュリティを実現するため、Google Playと直接通信するサーバーサイド検証の方が安全
RevenueCat の場合
RevenueCat は購入情報の検証を 自社のサーバーで自動的に実行してくれるため、開発者が purchaseToken の検証処理を書く必要はない。
不正購入のブロックやレシートの再検証、クロスプラットフォームの整合性も含めて SDK + Backend が管理してくれる。
詳細解説(getBuyIntent)
getBuyIntent メソッドは、指定された商品を購入するための購入フロー(Intent)を生成するための旧式のAPIであり、アプリがユーザーに課金UIを表示するために使用されていた。
このメソッドは Billing Library v2以前のもので、直接 IntentSender を取得して startIntentSenderForResult() などで課金画面を起動する設計だった。
Bundle buyIntentBundle = billingService.getBuyIntent(3, packageName, sku, itemType, developerPayload);
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
startIntentSenderForResult(pendingIntent.getIntentSender(), ..., ...);
現在の状況は非推奨(getBuyIntent)
getBuyIntent は完全に廃止予定であり、BillingClient ライブラリでは以下のような 非同期的な購入処理に置き換えられている:
val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(...)
.build()
val billingResult = billingClient.launchBillingFlow(activity, flowParams)
この新しい実装では、Google Play ストアアプリが直接UIを提供し、ユーザーが選択・認証・支払いを行う画面を表示する。
購入結果は PurchasesUpdatedListener 経由で通知される。
注意点(getBuyIntent)
getBuyIntentを使った実装は Play Console の審査で拒否される可能性が高く、現在は使用すべきでないlaunchBillingFlow()を使うことで、Googleの定めるユーザー体験基準を満たす形で課金処理を行えるBillingFlowParamsでは、商品ID、商品タイプ、オファリングID などが指定可能
RevenueCatの場合(getBuyIntent)
RevenueCatを利用している場合、購入フローのUI起動や launchBillingFlow() の呼び出しも SDK によって抽象化されている。
アプリ側は Purchases.shared.purchase(...) などの高レベルAPIを使って購入処理を実行するだけでよく、Google Play特有のフローやバージョン違いを意識する必要がない。
5. startIntentSenderForResult メソッド
PendingIntentとともに、startIntentSenderForResultを呼び出すと、決済画面が表示され、ここで利用者がアイテムの購入に同意すると決済処理が行われる。
詳細解説(startIntentSenderForResult)
startIntentSenderForResult は Android における汎用的なUI起動APIであり、IntentSender(=遅延実行可能なIntent)を使って外部のUI(この場合はGoogle Playの課金画面)を起動し、その結果を非同期で受け取るために使用される。
アプリ内課金においては、getBuyIntent() の戻り値として得られる PendingIntent から IntentSender を取り出し、このメソッドを使って課金画面を表示していた。
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
startIntentSenderForResult(
pendingIntent.getIntentSender(),
PURCHASE_REQUEST_CODE,
new Intent(),
0, 0, 0
);
UI上に表示されるのは Google Play ストアアプリの購入画面であり、利用者が「購入」などのボタンを押すことで決済処理が完了する。
購入結果は onActivityResult() を通じてアプリに返され、そこから responseCode と INAPP_PURCHASE_DATA を取得して検証を行っていた。
現在の扱い
BillingClient ライブラリ(v3以降)では startIntentSenderForResult を直接使うことは推奨されておらず、代わりに launchBillingFlow() を利用することで内部的に必要な画面遷移が行われるようになっている。
つまり、アプリ開発者が IntentSender を直接扱う必要はなくなり、BillingFlowParams を組み立てて渡すだけで課金画面の表示・完了通知を PurchasesUpdatedListener に任せられるようになっている。
注意点(startIntentSenderForResult)
startIntentSenderForResultの使用は今後のライブラリアップデートで非互換となる可能性がある- Jetpack Activity Result API とは併用しづらく、モダンな実装には向かない
- Google Play Billing ライブラリの新しい設計思想に従うことが望ましい
RevenueCatの場合(startIntentSenderForResult)
RevenueCatを利用している場合、startIntentSenderForResult のように IntentSender を手動で取得・起動する必要はない。
課金UIの起動・購入の完了・復元処理などのすべては RevenueCat SDK によって抽象化されており、たとえば次のようなコードだけで購入が行える
Purchases.shared.purchase(activity, storeProduct) { transaction, userCancelled, error ->
// 購入成功・キャンセル・エラーの処理をここで分岐
}
このように、アプリ開発者は Google Play のバージョン互換や IntentSender の管理を意識する必要がなくなり、プラットフォーム間の課金処理も統一的に扱えるのが利点となっている。