Google Play Billing は、Android におけるアプリ内購入やサブスクリプションを処理するための包括的な API を提供しています。ほとんどの開発者は、標準的な購入フローには慣れているでしょう。すなわち、課金フローを起動し、結果を受け取り、購入を承認し、エンタイトルメントを付与するという流れです。しかし、本番環境の課金システムでは、チュートリアルやサンプルコードでは十分に扱われない、より幅広いシナリオに対応する必要があります。保留中の購入(Pending purchases)、複数量の消耗型アイテム、日割り計算を伴うサブスクリプションのダウングレード、そして ITEM_ALREADY_OWNED レスポンスは、いずれも実際の環境で発生するケースであり、これらを誤って処理すると、収益の損失、ユーザーの混乱、購入失敗につながる可能性があります。
本記事では、Google Play Billing における代表的なエッジケースを取り上げ、それぞれが発生する理由を理解し、Play Billing Library を使って正しく処理する方法を確認します。また、RevenueCat がこれらのシナリオをどのように簡素化し、課金インフラではなくプロダクト開発に集中できるようにするのかも 見ていきます。
根本的な問題:ハッピーパスだけでは不十分
多くの課金実装は、Android の公式ドキュメントにあるサンプルコードから始まります。
1// The happy path
2billingClient.launchBillingFlow(activity, params)
3
4// In PurchasesUpdatedListener
5override fun onPurchasesUpdated(
6 billingResult: BillingResult,
7 purchases: List<Purchase>?
8) {
9 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
10 purchases?.forEach { purchase ->
11 if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
12 acknowledgePurchase(purchase)
13 grantEntitlement(purchase)
14 }
15 }
16 }
17}
これは、成功した即時購入を処理するケースを想定しています。しかし、ユーザーがコンビニで支払いを行い、支払い が48時間遅れる場合はどうでしょうか? 以前の承認処理が静かに失敗しており、ユーザーがすでにその商品を所有している場合はどうでしょうか? サブスクリプションのダウングレードが即時ではなく、次回の更新時に反映される場合はどうでしょうか? これらの各シナリオには個別の対応が必要であり、無視するとサポート問い合わせの増加、返金リクエスト、そしてサブスクライバーの離脱につながります。
保留中の購入(Pending purchases):支払いが即時ではない場合
すべての購入が即座に完了するわけではありません。コンビニでの現金支払い、銀行振込、一部のキャリア決済など、特定の支払い方法では非同期処理が必要になります。ユーザーがこれらの方法で購入を開始すると、Google Play は PURCHASED 状態では なく、PENDING 状態の購入として返します。
なぜ保留中の購入が発生するのか
保留中の購入は、クレジットカードの普及率が低い市場で一般的です。
| 支払い方法 | 一般的な地域 | 通常の処理時間 |
|---|---|---|
| 現金支払い(コンビニ) | 日本、メキシコ、インドネシア | 24〜48時間 |
| 銀行振込 | ドイツ、オランダ、ブラジル | 1〜3営業日 |
| キャリア決済(一部のキャリア) | 各国 | 数分〜数時間 |
アプリをグローバルに展開している場合、保留中の購入に遭遇するのは避けられません。この状態を無視すると、これらの地域のユーザーはそもそも商品を購入できなくなるか、あるいは購入が「消えた」ように見える混乱した挙動に直面することになります。
保留状態の検出と処理
PurchasesUpdatedListener は、完了した購入と同様に、保留中の購入も受け取ります。重要な違いは、 purchaseState フィールドにあります。
1override fun onPurchasesUpdated(
2 billingResult: BillingResult,
3 purchases: List<Purchase>?
4) {
5 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
6 purchases?.forEach { purchase ->
7 when (purchase.purchaseState) {
8 Purchase.PurchaseState.PURCHASED -> {
9 // Payment complete, grant access
10 processPurchase(purchase)
11 }
12 Purchase.PurchaseState.PENDING -> {
13 // Payment not yet complete
14 handlePendingPurchase(purchase)
15 }
16 Purchase.PurchaseState.UNSPECIFIED_STATE -> {
17 // Unknown state, query backend for clarification
18 queryBackendForState(purchase)
19 }
20 }
21 }
22 }
23}
重要なルールは次のとおりです。保留中の購入に対してエンタイトルメントを付与してはいけません。ユーザーはまだ支払いを完了していません。代わりに、保留中の購入を記録し、そのステータスを明確にユーザーへ伝える必要があります。
1fun handlePendingPurchase(purchase: Purchase) {
2 // Store the pending purchase token for later verification
3 purchaseRepository.savePendingPurchase(
4 purchaseToken = purchase.purchaseToken,
5 productId = purchase.products.first(),
6 orderId = purchase.orderId,
7 purchaseTime = purchase.purchaseTime,
8 )
9
10 // Show clear UI to the user
11 showPendingUI(
12 message = "Your purchase is being processed. " +
13 "You'll get access once payment is confirmed.",
14 )
15}

