🐸

Google Play Billing Library 5 チートシート

2022/10/27に公開約6,800字

Billing Library3から思い切って5に変えた所、色々変わり過ぎて驚いたので、コピペで使えそうな 「定期購入処理用」 のソースコードを解説コメントと共に記しておきます。未来の僕へ。

以下は記事執筆時点(2022/10/27)で最新のBilling Library 5.0.0で動作確認を行っています。

implementation 'com.android.billingclient:billing-ktx:5.0.0'

Billing Libraryのリリースノートはコチラ
https://developer.android.com/google/play/billing/release-notes

※1. ネスト警察に連行されそうなソースコードになっていますが、わかり易さ優先の為で、実際の僕のコードはちゃんとしている可能性があります。

※2. この記事を参考に実装して問題が起こったらコメント欄で教えて下さい。
僕のアプリを修正します(^o^)v

事前準備

Billing Libraryを利用するActivityやFragmentでは購入結果が帰ってくる「PurchasesUpdatedListener#onPurchasesUpdated」を実装しておきます。

課金処理を行うBillingClientのインスタンスを初期化しておきます。

private lateinit var billingClient: BillingClient

// hoge hoge

billingClient = BillingClient.newBuilder(context).enablePendingPurchases().setListener(this).build()
billingClient.startConnection(object: BillingClientStateListener {
   override fun onBillingSetupFinished(billingResult: BillingResult) {
      if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
         // Google Playに接続成功
      } else {
         // エラー. BAD END
      }
   }
   
   override fun onBillingServiceDisconnected() {
      // Google Playとの接続解除
      // 購入中に他のアプリ行かれちゃったり、何らかのエラーで接続が切れたり、するので監視して、再接続をするとbetter
   }
})

商品情報の取得

// 販売する商品のProductID(昔はSKUって呼んでたけどv5からproductIDに変わったみたい)リスト
var productList = mutableListOf(
   QueryProductDetailsParams.Product.newBuilder()
      .setProductId("jp.hoge.sample.product1")
      .setProductType(BillingClient.ProductType.SUBS)
      .build(),
      ....
)

// 以降、色んなparamが出てきて分かりづらい1 (QueryProductDetailsParams)
val params = QueryProductDetailsParams.newBuilder().setProductList(productList).build()

// 商品情報のリクエスト。v4あたりから非同期のAsyncばっかりに変わってます。
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
   if (billingResult!!.responseCode == BillingClient.BillingResponseCode.OK
         && !productDetailsList.isNullOrEmpty()) {
      // ・ 商品情報取得成功
      // ・ 購入処理で使うので、とっておくといいよ
      // ・ productDetailsList[0].subscriptionOfferDetails!![0].pricingPhases.pricingPhaseList[0].formattedPrice、で通貨単位付きの値段が取れるよ
   } else {
      // 商品情報取得失敗。BAD END
   }
}

購入

// 購入する商品のリスト。複数の商品を同時購入できるけど1つしか試したことない。
// 上記「商品情報の取得」でとっておいたProductDetailsをセット
// setOfferTokenはv5の新機能「特典(offer)」の情報だけど詳細は知らない。特典がなくてもセットしておかないとエラーになるので、このまま真似して、どうぞ
val productDetailsParamsList =
   listOf(
      BillingFlowParams.ProductDetailsParams.newBuilder()
         .setProductDetails(productDetails)                
	 .setOfferToken(productDetails.subscriptionOfferDetails!![0].offerToken)
	 .build()
   )
   
// 以降、色んなparamが出てきて分かりづらい2 (BillingFlowParams)
val billingFlowParams = BillingFlowParams.newBuilder()
   .setProductDetailsParamsList(productDetailsParamsList)
   
// サブスクの契約状況を確認したら、購入処理を走らせる
val purchaseType = QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
billingClient.queryPurchasesAsync(purchaseType)
{ billingResult, purchaseList ->
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
         && !purchaseList.isNullOrEmpty()) {

      // ココに来たってことは既にサブスク契約中
      // iOSと違ってアップグレード/ダウングレード/クロスグレードの処理を書かないとダメ
      // 書かない場合は複数の商品購入状態になって大混乱する
      // purchaseListには契約中の情報が入ってくる
      // ReplaceProrationModeについては、後述
      //
      // ※※※ ご注意 ※※※
      // ※複数商品の同時契約状態は想定していません
      // ※クロスグレードも想定していません 
      
      if (purchaseList!![0].products[0] == ${月額課金のproductID}
            && productDetails.productId == ${年額課金のproductID}) {
         
	 // アップグレード
         billingFlowParams.setSubscriptionUpdateParams(
            BillingFlowParams.SubscriptionUpdateParams.newBuilder()
	       // productIDじゃなくてpurchaseTokenですから
	       .setOldPurchaseToken(purchaseList!![0].purchaseToken)    
	       // 更新タイミングの指定。詳細は後述	       
	       .setReplaceProrationMode(BillingFlowParams.ProrationMode.DEFERRED)
	       .build()
         )
      } else if (purchaseList!![0].products[0] == ${年額課金のproductID}
            && productDetails.productId == ${月額課金のproductID}) {
                            
	 // ダウングレード
	 billingFlowParams.setSubscriptionUpdateParams(
            BillingFlowParams.SubscriptionUpdateParams.newBuilder()
	       // productIDじゃなくてpurchaseTokenですから
	       .setOldPurchaseToken(purchaseList!![0].purchaseToken)    
	       // 更新タイミングの指定。詳細は後述
	       .setReplaceProrationMode(BillingFlowParams.ProrationMode.DEFERRED)
	       .build()
	 )
      }
   }
   
   // 購入!
   // PurchasesUpdatedListener#onPurchasesUpdatedに購入結果が来る
   billingClient.launchBillingFlow(this, billingFlowParams.build())
}

ReplaceProrationMode


https://developer.android.com/google/play/billing/subscriptions?hl=ja

購入結果のハンドリング

override fun onPurchasesUpdated(
      billingResult: BillingResult,
      purchases: MutableList<Purchase>?) {
   
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
         && purchases.isNullOrEmpty().not()) {
      // 購入処理は成功
      //
      // ここでチート対策にレシート検証をするとbetter
      // PHPだと「Google_Service_AndroidPublisher」とかでググってみて

      if (!purchases[0].isAcknowledged) {
         // 未承認なので承認する。しないと3日ぐらいで自動キャンセルになる
     
         val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchaseList!![0].purchaseToken)
            .build()

         // 購入処理の承認		     
         billingClient.acknowledgePurchase(
            acknowledgePurchaseParams,
            AcknowledgePurchaseResponseListener {

               if (it.responseCode == BillingClient.BillingResponseCode.OK) {
                  // 無事成功です。お疲れ様でした
               } else {
                  // 承認処理に失敗。BAD END
               }
            }
	 )
      } else {
         // 承認済みなので、無事成功です。お疲れさまでした
      }
   } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
      // キャンセルされた
   } else {
      // 購入処理で何かエラーが起きてる。BAD END 
      // 以下のResponseCodeと照らし合わせると原因がわかる(かも)
      // https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode
   }
}

リストア

billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build())
{ billingResult, purchaseList ->
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
      if (purchaseList.isNullOrEmpty().not()) {
         // リストア成功
      } else {
         // 契約中の商品は無い
      }
   } else {
      // リストア中にエラー。BAD END
   }
}

参考

https://developer.android.com/google/play/billing/migrate-gpblv5?hl=ja

https://codelabs.developers.google.com/play-billing-codelab?hl=ja#0

https://developer.android.com/google/play/billing/compatibility?hl=ja

https://developer.android.com/google/play/billing/release-notes

Discussion

ログインするとコメントできます