#はじめに
Google Play Billing Library を使って、アプリ内課金を実装してみます。
このコードラボを参考にしてます。
https://github.com/googlecodelabs/play-billing-codelab
#課金の種類について
日常的に触れる課金は、以下のように分類されると思います。
-
消費型商品
使う度に消費されるアプリ内通貨や、回復アイテムなど -
非消費型商品
広告削除、機能のアンロックなど、一度購入したら永続的に使えるもの -
定期購入型商品
開発者が指定した期間毎に自動的に購入されるもの
Netflixなどの月額課金などと呼ばれるもの
ちなみに 消費型
と 非消費型
と分けましたが、これらは厳密に分類されてるわけではなく、
購入した商品を消費するか、永続的に持つかどうかの実装の違いしかありません。
#実装
##ライブラリの導入
build.gradleのdependenciesに1行追加しましょう
dependencies {
...
implementation 'com.android.billingclient:billing:1.2.1'
}
##BillingClientのセットアップ
fun setup() {
// ユーザーが商品を購入した時に呼ばれる
val purchaseUpdateListener = object : PurchasesUpdatedListener {
override fun onPurchasesUpdated(
responseCode: Int,
purchases: MutableList<Purchase>?
) {
Log.d("onPurchasesUpdated", "responseCode:$responseCode")
Log.d("onPurchasesUpdated", "purchase:${purchases?.toString()}")
purchases?.forEach { purchase ->
Log.d("onPurchasesUpdated", "purchase:$purchase")
}
}
}
// BillingClient : メインに使われるインターフェース
val billingClient: BillingClient = BillingClient
.newBuilder(Activity())
.setListener(purchaseUpdateListener)
.build()
// セットアップ処理のコールバック
val clientStateListener = object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.d("onBillingServiceDisconnected", "Service disconnected.")
}
override fun onBillingSetupFinished(responseCode: Int) {
if (responseCode == BillingClient.BillingResponse.OK) {
Log.d("onBillingSetupFinished", "Setup success. responseCode:$responseCode")
} else {
Log.w("onBillingSetupFinished", "Setup error. responseCode:$responseCode")
}
}
}
// 接続開始
billingClient.startConnection(clientStateListener)
}
##商品の詳細情報を取得
商品を購入するために、商品の詳細情報 SkuDetails
が必要になります。
###querySkuDetailsAsync
skuType
と sku
を指定して詳細情報を非同期で取得出来ます。
// BillingClient.SkuType.INAPP : アプリ内通貨や、消費アイテムなど
val skuType = BillingClient.SkuType.INAPP
// SKU : 固有の商品 ID
val inAppSkus = listOf("gas", "premium")
val params = SkuDetailsParams.newBuilder()
.setType(skuType)
.setSkusList(inAppSkus)
.build()
// 取得開始
billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
if (responseCode == BillingClient.BillingResponse.OK) {
Log.d(TAG, skuDetailsList.toString())
}
}
こんな感じのデータが返ってきます
D/querySkuDetailsAsync:
[SkuDetails: {
"productId": "gas",
"type": "inapp",
"price": "¥111",
"price_amount_micros": 110686158,
"price_currency_code": "JPY",
"title": "Gas (Play Billing Codelab)",
"description": "Buy gasoline to ride!"
},
SkuDetails: {
"productId": "premium",
"type": "inapp",
"price": "¥166",
"price_amount_micros": 166029237,
"price_currency_code": "JPY",
"title": "Upgrade your car (Play Billing Codelab)",
"description": "Buy a premium outfit for your car!"
}]
以下、この SkuDetails を使って進めていきます。
##商品を購入
// 購入フローを開始するためのパラメータ
val billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build()
// 購入フローを開始します
billingClient.launchBillingFlow(activity, billingFlowParams)
購入後に PurchasesUpdatedListener#onPurchasesUpdated()
が呼ばれます。
D/onPurchasesUpdated: responseCode:0 // BillingClient.BillingResponse.OK
D/onPurchasesUpdated: purchase:[Purchase. Json: // 購入した商品の情報が返ってくる
{
"orderId": ...,
"packageName": com.codelab.sample,
"productId": "premium", // SKU
"purchaseTime": ...,
"purchaseState": ..., // 注文の購入状況 (0:Purchased, 1:Canceled)
"purchaseToken" ...:
}
追加でもう一度購入したいので、再び購入処理を行ってみると
W/BillingClient: Unable to buy item, Error response code: 7
エラーログが出てる…エラーコードを確認しましょう。
/** Failure to purchase since item is already owned */
int ITEM_ALREADY_OWNED = 7;
既に所有しているとのこと。
##商品を消費
商品を再び買うためには、購入済みの商品を 消費
しなければいけません。
BillingClient#consumeAsync (String purchaseToken, ConsumeResponseListener listener)
を呼んで消費しましょう。
(※SkuType.SUBS の定期購入型商品は消費出来ません。)
###purchaseToken の取得
購入後に呼ばれる onPurchasesUpdated
に purchaseToken
含む情報が返ってきてましたが、保存していなくても後から BillingClient#queryPurchases()
で問い合わせる事が出来ます。
fun queryInAppPurchases() {
// アプリ内で購入した指定 SkuType の商品の購入詳細を取得します
val purchaseResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
// ログ確認
Log.d("queryPurchases", "responseCode:${purchaseResult.responseCode}")
Log.d("queryPurchases", purchaseResult.purchasesList.toString())
}
以下のようなデータが返ってきます。
D/queryPurchases: responseCode:0 // BillingClient.BillingResponse.OK
D/queryPurchases: Purchase. Json:
{
"orderId": ...,
"packageName": com.codelab.sample,
"productId": "premium", // SKU
"purchaseTime": ...,
"purchaseState": ..., // 注文の購入状況 (0:Purchased, 1:Canceled)
"purchaseToken" ...:
}
もしアプリをアンインストールしてしまって、購入情報を失ってしまった場合も、購入時のGoogleアカウントが紐付いていれば、この情報を使って購入済みの状態を復元する事が出来そうです。
####purchaseToken を使って商品を消費する
fun consume(purchaseToken: String) {
// 消費操作が終了した時に呼ばれるリスナー
val consumeResponseListener = object : ConsumeResponseListener{
override fun onConsumeResponse(responseCode: Int, purchaseToken: String?) {
Log.d("onConsumeResponse", "responseCode:$responseCode")
Log.d("onConsumeResponse", "purchaseToken:$purchaseToken")
}
}
// purchaseToken とリスナーを渡して消費処理開始
billingClient.consumeAsync(purchaseToken, consumeResponseListener)
}
消費ログを確認
D/onConsumeResponse: responseCode:0 // BillingClient.BillingResponse.OK
D/onConsumeResponse: purchaseToken:......
消費に成功してそうなので、もう一度 BillingClient#queryPurchases()
で購入情報を確認してみましょう。
D/queryPurchases: responseCode:0
D/queryPurchases: []
リストが空になってますね。
購入してる商品が無い状態なので、購入処理を再度呼んでみます。
D/onPurchasesUpdated:responseCode:0 // BillingClient.BillingResponse.OK
D/onPurchasesUpdated: purchase:[Purchase. Json:
{
"orderId": ...,
"packageName": com.codelab.sample,
"productId": "premium",
"purchaseTime": ...,
"purchaseState": ...,
"purchaseToken" ...:
}
購入成功しました。
アプリ内課金の基本的な流れは以上となります。
##BillingClientをもう少し使ってみる
アプリ内課金のための便利な方法を提供してくれます。
他のメソッドも使ってみましょう。
###queryPurchaseHistoryAsync
購入が期限切れ、キャンセル、または消費された場合でも、ユーザーによって行われた最新の購入を返します。
fun queryPurchaseHistoryAsync() {
// リスナーを用意
val purchaseHistoryResponseListener = object :PurchaseHistoryResponseListener{
override fun onPurchaseHistoryResponse(responseCode: Int,
purchasesList: MutableList<Purchase>?) {
Log.d(TAG, "responseCode:$responseCode")
Log.d(TAG, purchasesList?.toString())
}
}
// SkuTypeとリスナーを渡して呼び出し
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, purchaseHistoryResponseListener)
}
レスポンス
D/BillingManager: responseCode:0 // BillingClient.BillingResponse.OK
D/BillingManager: [Purchase.Json: {
"productId": "gas",
"purchaseToken": ......
"purchaseTime": ......
"developerPayload": ......
}, Purchase.Json: {
"productId": "premium",
"purchaseToken": ......
"purchaseTime": ......
"developerPayload": ......
}]
同じ商品を複数回買っていた場合、最新の購入情報が返ってきます。
###launchPriceChangeConfirmationFlow
ユーザが購読している商品の価格が変わったとき、このフローを起動してユーザに価格変更情報のある画面を表示させます。ユーザーは新しい価格を確認するか、またはフローをキャンセルすることができます。
※↓の処理の前に sku == "gold_monthly"
の定期購読商品を定期購入しています。
fun launchPriceChangeConfirmationFlow() {
// PriceChangeFlowParams の生成に SkuDetails が必要なので、取得します
val skuType = BillingClient.SkuType.SUBS
val subsSkus = listOf("gold_monthly", "gold_yearly")
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setType(skuType)
.setSkusList(subsSkus)
.build()
// SkuDetailsを取得する
billingClient.querySkuDetailsAsync(skuDetailsParams) { responseCode, skuDetailsList ->
// SkuType.SUBS の SkuDetailsList から SKU が gold_monthly の SkuDetails を取る
val gold = skuDetailsList.filter { it.sku == "gold_monthly" }[0]
// パラメータを生成する
val priceChangeFlowParams = PriceChangeFlowParams.newBuilder()
.setSkuDetails(gold)
.build()
// 価格変更確認処理を開始する
billingClient.launchPriceChangeConfirmationFlow(activity, priceChangeFlowParams) {responseCode ->
Log.d(TAG, "responseCode:$responseCode")
}
}
}
実際に価格変更は試していませんが、
価格の変更をユーザーに知らせて、契約更新を選べるんでしょうか。
##関連リンク
https://developer.android.com/google/play/billing/billing_library_overview
https://developer.android.com/google/play/billing/billing_onetime
https://github.com/googlecodelabs/play-billing-codelab
以上です。