Share
Embed
[DroidKaigi 2020] Re:ゼロから始める Play Billing Library / Re: Zero - starting uses of Play Billing Library
syarihu
February 21, 2020
Transcript
227 slides
-
アジェンダ • Play Billing Library概要 • Play Billing Library 2.0新機能
• Purchase Anywhere • AACを利用したPlay Billing Libraryによる課金実装 • 年額課金を実装したときに得ら れた知見 -
• AndroidではIn-app Billing APIを通じてPlay Billingと連携し、アプリ課金を実現する • Play Billing Libraryの登場以前はAIDLを使っ てアプリ課金の実装を行っていた
• AIDLではアプリからサービスへの接続や通信 の管理をすべて行う AIDL -
通常の支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 -
通常の支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 -
通常の支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 -
通常の支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 -
通常の支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 -
通常の支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 Google Playでの購入は正常にできているのに サービスは利用できない状態になってしまう -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 承認リクエスト 承認結果 -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 承認リクエスト 承認結果 購入情報の登録に失敗しても 3日以内に承認されない場合は払い戻されるので安心 -
acknowledgeに対応した支払いフロー(アプリ) ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー(アプリ) ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー(アプリ) ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー(アプリ) ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー(アプリ) ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 アプリで通信失敗したときなどの考慮は必要 -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
Google Playへの支払いリクエスト ユーザー アプリ サーバー 支払いリクエストを行う 支払いを促す Google Play 支払いコード
レシート送信 レシート検証 購入情報を登録 購入結果 レシート(支払い保留状態) -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
コンビニへの支払い コンビニ ユーザー アプリ サーバー 支払いを行う 支払い完了通知 承認リクエスト 承認結果 アイテムを渡す
Google Play 支払い完了通知 購入履歴をリクエスト レシート検証 購入情報を登録 購入結果 アプリを起動 購入履歴 レシートを送信 -
• Promo Codes • Gift Cards • Google Play Points
• Subscribe and Install アプリ外購入 -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { private val billingClient:
BillingClient = BillingClient.newBuilder(application) .enablePendingPurchases() .setListener(this /* PurchasesUpdatedListener */) .build() } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { private val billingClient:
BillingClient = BillingClient.newBuilder(application) .enablePendingPurchases() .setListener(this /* PurchasesUpdatedListener */) .build() } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { private val billingClient:
BillingClient = BillingClient.newBuilder(application) .enablePendingPurchases() .setListener(this /* PurchasesUpdatedListener */) .build() } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { private val billingClient:
BillingClient = BillingClient.newBuilder(application) .enablePendingPurchases() .setListener(this /* PurchasesUpdatedListener */) .build() } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { private val billingClient:
BillingClient = BillingClient.newBuilder(application) .enablePendingPurchases() .setListener(this /* PurchasesUpdatedListener */) .build() } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { ...省略... /** 購入完了、購入キャンセルなどのコールバック
*/ override fun onPurchasesUpdated( billingResult: BillingResult, purchases: MutableList<Purchase>? ) { } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { private val billingClient:
BillingClient = BillingClient.newBuilder(application) .enablePendingPurchases() .setListener(this /* PurchasesUpdatedListener */) .build() ...省略... } BillingViewModel.kt -
sealed class BillingStatus { object SetupSuccess : BillingStatus() object BillingFlow
: BillingStatus() object PendingPurchase : BillingStatus() object AcknowledgeSuccess : BillingStatus() object ServiceDisconnected : BillingStatus() object NoPreviousPlanPurchaseHistory : BillingStatus() data class Error(val billingResult: BillingResult) : BillingStatus() } BillingStatus.kt -
sealed class BillingStatus { object SetupSuccess : BillingStatus() object BillingFlow
: BillingStatus() object PendingPurchase : BillingStatus() object AcknowledgeSuccess : BillingStatus() object ServiceDisconnected : BillingStatus() object NoPreviousPlanPurchaseHistory : BillingStatus() data class Error(val billingResult: BillingResult) : BillingStatus() } BillingStatus.kt -
sealed class BillingStatus { object SetupSuccess : BillingStatus() object BillingFlow
: BillingStatus() object PendingPurchase : BillingStatus() object AcknowledgeSuccess : BillingStatus() object ServiceDisconnected : BillingStatus() object NoPreviousPlanPurchaseHistory : BillingStatus() data class Error(val billingResult: BillingResult) : BillingStatus() } BillingStatus.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { ...省略... /** 購入完了、購入キャンセルなどのコールバック
*/ override fun onPurchasesUpdated( billingResult: BillingResult, purchases: MutableList<Purchase>? ) { } } BillingViewModel.kt -
sealed class BillingStatus { object SetupSuccess : BillingStatus() object BillingFlow
: BillingStatus() object PendingPurchase : BillingStatus() object AcknowledgeSuccess : BillingStatus() object ServiceDisconnected : BillingStatus() object NoPreviousPlanPurchaseHistory : BillingStatus() data class Error(val billingResult: BillingResult) : BillingStatus() } BillingStatus.kt -
sealed class BillingStatus { object SetupSuccess : BillingStatus() object BillingFlow
: BillingStatus() object PendingPurchase : BillingStatus() object AcknowledgeSuccess : BillingStatus() object ServiceDisconnected : BillingStatus() object NoPreviousPlanPurchaseHistory : BillingStatus() data class Error(val billingResult: BillingResult) : BillingStatus() } BillingStatus.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { ...省略... private val
_billingStatus = MutableLiveData<BillingStatus>() val billingStatus: LiveData<BillingStatus> = _billingStatus } BillingViewModel.kt -
Google Playの課金サービスに接続する ViewModel BillingClient Google Play startConnection onServiceConnected bindService onBillingSetupFinished
Fragment OnLifecycleEvent: onCreate LiveData: SetupSuccess -
Google Playの課金サービスに接続する ViewModel BillingClient Google Play startConnection onServiceConnected bindService onBillingSetupFinished
Fragment OnLifecycleEvent: onCreate LiveData: SetupSuccess -
Google Playの課金サービスに接続する ViewModel BillingClient Google Play startConnection onServiceConnected bindService onBillingSetupFinished
Fragment OnLifecycleEvent: onCreate LiveData: SetupSuccess -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... private
fun startConnection() { billingClient.startConnection(this /* BillingClientStateListener */) } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... private
fun startConnection() { billingClient.startConnection(this /* BillingClientStateListener */) } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... private
fun startConnection() { billingClient.startConnection(this /* BillingClientStateListener */) } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... override
fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) _billingStatus.postValue(BillingStatus.SetupSuccess) else _billingStatus.postValue(BillingStatus.Error(billingResult)) } override fun onBillingServiceDisconnected() = Unit } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... override
fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) _billingStatus.postValue(BillingStatus.SetupSuccess) else _billingStatus.postValue(BillingStatus.Error(billingResult)) } override fun onBillingServiceDisconnected() = Unit } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... override
fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) _billingStatus.postValue(BillingStatus.SetupSuccess) else _billingStatus.postValue(BillingStatus.Error(billingResult)) } override fun onBillingServiceDisconnected() = Unit } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... override
fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) _billingStatus.postValue(BillingStatus.SetupSuccess) else _billingStatus.postValue(BillingStatus.Error(billingResult)) } override fun onBillingServiceDisconnected() = Unit } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... override
fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) _billingStatus.postValue(BillingStatus.SetupSuccess) else _billingStatus.postValue(BillingStatus.Error(billingResult)) } override fun onBillingServiceDisconnected() = Unit } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener { ...省略... override
fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) _billingStatus.postValue(BillingStatus.SetupSuccess) else _billingStatus.postValue(BillingStatus.Error(billingResult)) } override fun onBillingServiceDisconnected() = Unit } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener, LifecycleObserver { ...省略...
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun onCreate() { startConnection() } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener, LifecycleObserver { ...省略...
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun onCreate() { startConnection() } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener, LifecycleObserver { override
fun onCleared() { super.onCleared() billingClient.endConnection() } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener, LifecycleObserver { override
fun onCleared() { super.onCleared() billingClient.endConnection() } } BillingViewModel.kt -
アイテムの購入画面を表示するまでの流れ ユーザー アプリ Google Play アイテムの購入ボタンをタップ SkuDetails アイテムの購入画面を表示 SkuDetailsを元に 購入をリクエスト
アイテムIDとアイテムの種類を元に アイテム情報をリクエスト -
enum class BillingSkuType( @BillingClient.SkuType val skuType: String ) { /**
定期購読 */ Subscription(BillingClient.SkuType.SUBS), /** 消費不可のアプリ内アイテム */ InAppNonConsumable(BillingClient.SkuType.INAPP), /** 消費可能なアプリ内アイテム */ InAppConsumable(BillingClient.SkuType.INAPP), } BillingSkuType.kt -
enum class BillingItem(val sku: String, val billingSkuType: BillingSkuType) { MonthlyPlan(
"monthly_plan", BillingSkuType.Subscription), MonthlyPremiumPlan( "monthly_premium_plan", BillingSkuType.Subscription), NonConsumableItem( "non_consumable_item", BillingSkuType.InAppNonConsumable), ConsumableItem( "consumable_item", BillingSkuType.InAppConsumable), AndroidTestPurchased( "android.test.purchased", BillingSkuType.InAppNonConsumable); } BillingSkuType.kt -
enum class BillingItem(val sku: String,val billingSkuType: BillingSkuType) { ...省略... fun
skusList(): List<String> = listOf(sku) companion object { fun findBySku(sku: String): BillingItem? = values().find { it.sku == sku } } } BillingSkuType.kt -
private fun getSkuDetails( billingItem: BillingItem, onResponse: (skuDetails: SkuDetails) -> Unit
) { val skuDetailsParams = SkuDetailsParams.newBuilder() .setSkusList(billingItem.skusList()) .setType(billingItem.billingSkuType.skuType) .build() ...省略... BillingViewModel.kt -
billingClient.querySkuDetailsAsync(skuDetailsParams ) { billingResult: BillingResult, skuDetailList: List<SkuDetails> -> if (billingResult.responseCode
== BillingClient.BillingResponseCode.OK && skuDetailList.isNotEmpty() ) { onResponse(skuDetailList[0]) } else { _billingStatus.postValue(BillingStatus.Error(billingResult)) } } } BillingViewModel.kt -
billingClient.querySkuDetailsAsync(skuDetailsParams ) { billingResult: BillingResult, skuDetailList: List<SkuDetails> -> if (billingResult.responseCode
== BillingClient.BillingResponseCode.OK && skuDetailList.isNotEmpty() ) { onResponse(skuDetailList[0]) } else { _billingStatus.postValue(BillingStatus.Error(billingResult)) } } } BillingViewModel.kt -
billingClient.querySkuDetailsAsync(skuDetailsParams ) { billingResult: BillingResult, skuDetailList: List<SkuDetails> -> if (billingResult.responseCode
== BillingClient.BillingResponseCode.OK && skuDetailList.isNotEmpty() ) { onResponse(skuDetailList[0]) } else { _billingStatus.postValue(BillingStatus.Error(billingResult)) } } } BillingViewModel.kt -
billingClient.querySkuDetailsAsync(skuDetailsParams ) { billingResult: BillingResult, skuDetailList: List<SkuDetails> -> if (billingResult.responseCode
== BillingClient.BillingResponseCode.OK && skuDetailList.isNotEmpty() ) { onResponse(skuDetailList[0]) } else { _billingStatus.postValue(BillingStatus.Error(billingResult)) } } } BillingViewModel.kt -
fun purchase(activity: Activity, billingItem: BillingItem) { getSkuDetails(billingItem) { skuDetails ->
val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .build() _billingStatus.postValue(BillingStatus.BillingFlow) billingClient.launchBillingFlow(activity, flowParams) } } BillingViewModel.kt -
class BillingFragment : Fragment(), View.OnClickListener { ...省略... override fun onClick(v:
View?) { billingViewModel.purchase( requireActivity(), BillingItem.NonConsumableItem ) } } BillingFragment.kt -
支払いをしてからアイテムを受け取るまでの流れ ユーザー アプリ サーバー 支払いを行う レシート アイテムを渡す Google Play レシートを送信
レシート検証 購入情報を登録 購入結果 -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { ...省略... /** 購入完了、購入キャンセルなどのコールバック
*/ override fun onPurchasesUpdated( billingResult: BillingResult, purchases: MutableList<Purchase>? ) { } } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), PurchasesUpdatedListener { ...省略... /** 購入完了、購入キャンセルなどのコールバック
*/ override fun onPurchasesUpdated( billingResult: BillingResult, purchases: MutableList<Purchase>? ) { } } BillingViewModel.kt -
class BillingFragment : Fragment(), View.OnClickListener { ...省略... override fun onClick(v:
View?) { billingViewModel.purchase( requireActivity(), BillingItem.MonthlyPlan ) } } BillingFragment.kt -
class BillingFragment : Fragment(), View.OnClickListener { ...省略... override fun onClick(v:
View?) { billingViewModel.purchase( requireActivity(), BillingItem.MonthlyPlan ) } } BillingFragment.kt -
fun changePlan( activity: Activity, billingItem: BillingItem, oldBillingItem: BillingItem ) {
billingClient.queryPurchaseHistoryAsync( BillingClient.SkuType.SUBS) { _, purchases -> val purchase: PurchaseHistoryRecord? = purchases.find { it.sku == oldBillingItem.sku } if (purchase == null) { _billingStatus.postValue( BillingStatus.NoPreviousPlanPurchaseHistory) return@queryPurchaseHistoryAsync } BillingViewModel.kt -
fun changePlan( activity: Activity, billingItem: BillingItem, oldBillingItem: BillingItem ) {
billingClient.queryPurchaseHistoryAsync( BillingClient.SkuType.SUBS) { _, purchases -> val purchase: PurchaseHistoryRecord? = purchases.find { it.sku == oldBillingItem.sku } if (purchase == null) { _billingStatus.postValue( BillingStatus.NoPreviousPlanPurchaseHistory) return@queryPurchaseHistoryAsync } BillingViewModel.kt -
fun changePlan( activity: Activity, billingItem: BillingItem, oldBillingItem: BillingItem ) {
billingClient.queryPurchaseHistoryAsync( BillingClient.SkuType.SUBS) { _, purchases -> val purchase: PurchaseHistoryRecord? = purchases.find { it.sku == oldBillingItem.sku } if (purchase == null) { _billingStatus.postValue( BillingStatus.NoPreviousPlanPurchaseHistory) return@queryPurchaseHistoryAsync } BillingViewModel.kt -
fun changePlan( activity: Activity, billingItem: BillingItem, oldBillingItem: BillingItem ) {
billingClient.queryPurchaseHistoryAsync( BillingClient.SkuType.SUBS) { _, purchases -> val purchase: PurchaseHistoryRecord? = purchases.find { it.sku == oldBillingItem.sku } if (purchase == null) { _billingStatus.postValue( BillingStatus.NoPreviousPlanPurchaseHistory) return@queryPurchaseHistoryAsync } BillingViewModel.kt -
billingClient.queryPurchaseHistoryAsync( BillingClient.SkuType.SUBS) { _, purchases -> ...省略... getSkuDetails(billingItem) { skuDetails
-> val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .setOldSku(purchase.sku, purchase.purchaseToken) .build() _billingStatus.postValue(BillingStatus.BillingFlow) billingClient.launchBillingFlow(activity, flowParams) } BillingViewModel.kt -
billingClient.queryPurchaseHistoryAsync( BillingClient.SkuType.SUBS) { _, purchases -> ...省略... getSkuDetails(billingItem) { skuDetails
-> val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .setOldSku(purchase.sku, purchase.purchaseToken) .build() _billingStatus.postValue(BillingStatus.BillingFlow) billingClient.launchBillingFlow(activity, flowParams) } BillingViewModel.kt -
class BillingFragment : Fragment(), View.OnClickListener { ...省略... override fun onClick(v:
View?) { billingViewModel.changePlan( requireActivity(), billingItem = BillingItem.MonthlyPremiumPlan, oldBillingItem = BillingItem.MonthlyPlan ) } } BillingFragment.kt -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
acknowledgeに対応した支払いフロー ユーザー アプリ サーバー 支払いを行う レシート 承認リクエスト 承認結果 アイテムを渡す Google
Play レシートを送信 レシート検証 購入情報を登録 購入結果 -
private fun acknowledgePurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
purchase.isAcknowledged.not()) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase( acknowledgePurchaseParams, this /* AcknowledgePurchaseResponseListener */ ) } BillingViewModel.kt -
private fun acknowledgePurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
purchase.isAcknowledged.not()) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase( acknowledgePurchaseParams, this /* AcknowledgePurchaseResponseListener */ ) } BillingViewModel.kt -
private fun acknowledgePurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
purchase.isAcknowledged.not()) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase( acknowledgePurchaseParams, this /* AcknowledgePurchaseResponseListener */ ) } BillingViewModel.kt -
private fun acknowledgePurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
purchase.isAcknowledged.not()) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase( acknowledgePurchaseParams, this /* AcknowledgePurchaseResponseListener */ ) } BillingViewModel.kt -
class BillingViewModel(application: Application) : ViewModel(), BillingClientStateListener, PurchasesUpdatedListener, LifecycleObserver, AcknowledgePurchaseResponseListener {
override fun onAcknowledgePurchaseResponse(billingResult: BillingResult) { if (billingResult.responseCode() == BillingClient.BillingResponseCode.OK) { _billingStatus.postValue(BillingStatus.AcknowledgeSuccess) } else { _billingStatus.postValue(BillingStatus.Error(billingResult)) } } } BillingViewModel.kt -
プラン変更時の支払いの流れ ユーザー アプリ Google Play 支払いを行う レシート プランを利用可能にする 以前のプランを解 約
新しいプランを契 約 プラン変更後3日以内に承認しなかった場合は? -
プラン変更後3日以内に承認しなかった場合 ユーザー アプリ Google Play 支払いを行う レシート プランを利用可能にする 以前のプランを解 約
新しいプランを契 約 3日以内に承認 されていない 払い戻し -
プラン変更後3日以内に承認しなかった場合 ユーザー アプリ Google Play 支払いを行う レシート プランを利用可能にする 以前のプランを解 約
新しいプランを契 約 3日以内に承認 されていない 払い戻し -
プラン変更後3日以内に承認しなかった場合 ユーザー アプリ Google Play 支払いを行う レシート プランを利用可能にする 以前のプランを解 約
新しいプランを契 約 3日以内に承認 されていない 払い戻し ユーザーが新たにプランを契約し、 その契約が承認されない状態でプランを変更した場合は? -
private fun acknowledgePurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
purchase.isAcknowledged.not()) { ...省略... } else if ( purchase.purchaseState == Purchase.PurchaseState.PENDING ) { _billingStatus.postValue(BillingStatus.PendingPurchase) } } BillingViewModel.kt -
class BillingFragment : Fragment(), View.OnClickListener { override fun onViewCreated(view: View,
savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) billingViewModel.billingStatus.observe( viewLifecycleOwner, Observer { billingStatus -> when (billingStatus) { BillingStatus.SetupSuccess -> { // 購入履歴を取得して承認する } } }) } BillingFragment.kt -
1. aidlファイルを削除 2. Play Billing Libraryを導入 3. Play Billing Libraryによる課金実装
4. 古い定形コードを削除 AIDLからPBLに置き換えた話 -
月額プラン 500円 / 月 プラン変更 2019年04月15日 年額プラン 5,500円 / 年
支払い日 2019年04月01日 -
月額プラン 500円 / 月 プラン変更 2019年04月15日 年額プラン 5,500円 / 年
支払い日 2019年04月01日 500円 / 30日 = 16.66円 / 日 250円分使用 -
月額プラン 500円 / 月 プラン変更 2019年04月15日 年額プラン 5,500円 / 年
支払い日 2019年04月01日 500円 / 30日 = 16.66円 / 日 250円分使用 前プランの残り = 250円 -
月額プラン 500円 / 月 プラン変更 2019年04月15日 年額プラン 5,500円 / 年
支払い日 2019年04月01日 500円 / 30日 = 16.66円 / 日 250円分使用 前プランの残り = 250円 5,500円 / 365日 = 15.07円 / 日 -
月額プラン 500円 / 月 プラン変更 2019年04月15日 年額プラン 5,500円 / 年
支払い日 2019年04月01日 500円 / 30日 = 16.66円 / 日 250円分使用 前プランの残り = 250円 5,500円 / 365日 = 15.07円 / 日 250円 / 15.07円 = 約16日分使用可能 -
月額プラン 500円 / 月 プラン変更 2019年04月15日 年額プラン 5,500円 / 年
支払い日 2019年04月01日 500円 / 30日 = 16.66円 / 日 250円分使用 前プランの残り = 250円 5,500円 / 365日 = 15.07円 / 日 250円 / 15.07円 = 約16日分使用可能 新プラン支払い開始 2019年05月02日 -
• The “Purchase Anywhere” Paradigm Shift (Android Dev Summit '19)
◦ https://youtu.be/L2aiHRD6Pfk • Required Updates and New Functionality (Android Dev Summit '19) ◦ https://youtu.be/Cj5vq1AOJeQ • What's New with Google Play Billing (Google I/O'19) ◦ https://youtu.be/N4004Set4F8 参考資料