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
事前準備
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?) {
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
}
}
参考
Google Play Billing Library 4 から 5 への移行ガイド
https://developer.android.com/google/play/billing/migrate-gpblv5?hl=ja
Play Billing Library 5 を使用してアプリ内で定期購入を販売する
https://codelabs.developers.google.com/play-billing-codelab?hl=ja#0
2022 年 5 月の定期購入変更ガイド
https://developer.android.com/google/play/billing/compatibility?hl=ja
Google Play Billing Library リリースノート
https://developer.android.com/google/play/billing/release-notes