在今年的2023/11/01,Google要求新架上有使用Google Billing Api的App都必须更新到v5,或者也可以升级到v6,这样可以在两年内不必被要求更新
而当中有一些异动就是属于acknowledgePurchase
的部份
acknowledge Purchase
这是什么呢? 当你跳出系统选单的时候,在使用者确认购买后,经过处理取得订单流程后,原本你必须取得purchaseToken
透过
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build()billingClient.acknowledgePurchase(params) { billingResult ->response = BillingResponse(billingResult.responseCode)bResult = billingResult}
再次送出验证给Google,这样订单才会成立,否则在三天后Google会自动取消你的订单,这样可以避免在订阅过程后如果刚好App Crash,或是Response传到 App时候发生了网路错误,导致App没有处理到该笔订单的结果,让使用者无法正常的使用到商品或者避免使用者以为没有购买成功,但实际背景在自动进行订阅期间了
而他的responseCode
可以看到是这样定义的
@JvmInlineprivate value class BillingResponse(val code: Int) { val isOk: Boolean get() = code == BillingClient.BillingResponseCode.OK val canFailGracefully: Boolean get() = code == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED val isRecoverableError: Boolean get() = code in setOf( BillingClient.BillingResponseCode.ERROR, BillingClient.BillingResponseCode.SERVICE_DISCONNECTED, ) val isNonrecoverableError: Boolean get() = code in setOf( BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE, BillingClient.BillingResponseCode.BILLING_UNAVAILABLE, BillingClient.BillingResponseCode.DEVELOPER_ERROR, ) val isTerribleFailure: Boolean get() = code in setOf( BillingClient.BillingResponseCode.ITEM_UNAVAILABLE, BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED, BillingClient.BillingResponseCode.ITEM_NOT_OWNED, BillingClient.BillingResponseCode.USER_CANCELED, )}
但这件事情为什么要额外拿出来讲呢,主要就是v6版本开始建议二次确认这件事情,会需要由你在取得订单后,需要先将订单资讯抛送到你自己的server backend
在google的官方sample中有这样的範例可以参考,他是透过typescript与nodejs 等等并且串接firebase cloud function来进行实作,当然你也可以转成自己的server架构,不过说实在的这次的sample这样就变得相当複杂
https://github.com/android/play-billing-samples/tree/master/ClassyTaxiServer
当中订单是需要跟自己的帐号做关联的
可以再sample code看到google是这样标示
Acknowledge a purchase. * * https://developer.android.com/google/play/billing/billing_library_releases_notes#2_0_acknowledge * * Apps should acknowledge the purchase after confirming that the purchase token * has been associated with a user. This app only acknowledges purchases after * successfully receiving the subscription data back from the server. * * Developers can choose to acknowledge purchases from a server using the * Google Play Developer API. The server has direct access to the user database, * so using the Google Play Developer API for acknowledgement might be more reliable. * TODO(134506821): Acknowledge purchases on the server. * TODO: Remove client side purchase acknowledgement after removing the associated tests. * If the purchase token is not acknowledged within 3 days, * then Google Play will automatically refund and revoke the purchase. * This behavior helps ensure that users are not charged for subscriptions unless the * user has successfully received access to the content. * This eliminates a category of issues where users complain to developers * that they paid for something that the app is not giving to them.
当中重点就是
* TODO(134506821): Acknowledge purchases on the server.* TODO: Remove client side purchase acknowledgement after removing the associated tests.
在他们之前将会将billingClient中移除,需要以后都在server端进行验证,并且提到使用server端进行二次验证会更有可信赖度
这样的行为可以确保有成功购买前不会被收费
This behavior helps ensure that users are not charged for subscriptions unless theuser has successfully received access to the content.
因此会有这样的机制,而看起来目前v6还保留在client api中,但在未来将会移除这个可以在app端二次确认的呼叫,目前在sample中也只有留下function而没有呼叫使用
而App端的kotlin sample code可以参考这的部分
https://github.com/android/play-billing-samples/blob/master/ClassyTaxiAppKotlin/README.md
因此记得在这次的修改,必须增加能透过server自动进行订单二次确认,如果server遇到当下错误,会在商品资讯中得知他是否有进行二次确认了
var isAcknowledged: Boolean = false,
完整的class为
/** * Local subscription data. This is stored on disk in a database. */@Entity(tableName = "subscriptions")data class SubscriptionStatus( // Local fields. @PrimaryKey(autoGenerate = true) var primaryKey: Int = 0, var subscriptionStatusJson: String? = null, var subAlreadyOwned: Boolean = false, var isLocalPurchase: Boolean = false, // Remote fields. var product: String? = null, var purchaseToken: String? = null, var isEntitlementActive: Boolean = false, var willRenew: Boolean = false, var activeUntilMillisec: Long = 0, var isGracePeriod: Boolean = false, var isAccountHold: Boolean = false, var isPaused: Boolean = false, var isAcknowledged: Boolean = false, var autoResumeTimeMillis: Long = 0) { companion object { /** * Create a record for a subscription that is already owned by a different user. * * The server does not return JSON for a subscription that is already owned by * a different user, so we need to construct a local record with the basic fields. */ fun alreadyOwnedSubscription( product: String, purchaseToken: String ): SubscriptionStatus { return SubscriptionStatus().apply { this.product = product this.purchaseToken = purchaseToken isEntitlementActive = false subAlreadyOwned = true } } }}
主要就是可以得知这个订购商品目前的状态,这边简单流程就是,当透过billingClient
从google取得到purchaseToke
n后,必须先呼叫registerSubscription
跟自己的server注册这组订单,这时候会回传刚刚上面那个response data class,当中sample中会执行updateSubscriptionsFromNetwork()
/** * Update the local database with the subscription status from the remote server. * This method is called when the app starts and when the user refreshes the subscription * status. */ suspend fun updateSubscriptionsFromNetwork(remoteSubscriptions: List<SubscriptionStatus>?) { Log.i(TAG, "Updating subscriptions from remote: ${remoteSubscriptions?.size}") val currentSubscriptions = subscriptions.value val purchases = billingClientLifecycle.subscriptionPurchases.value // Acknowledge the subscription if it is not. kotlin.runCatching { remoteSubscriptions?.let { this.acknowledgeRegisteredSubscriptionPurchaseTokens(it) } }.onFailure { Log.e( TAG, "Failed to acknowledge registered subscription purchase tokens: " + "$it" ) }.onSuccess { acknowledgedSubscriptions -> Log.i( TAG, "Successfully acknowledged registered subscription purchase tokens: " + "$acknowledgedSubscriptions" ) val mergedSubscriptions = mergeSubscriptionsAndPurchases( currentSubscriptions, acknowledgedSubscriptions, purchases ) // Store the subscription information when it changes. localDataSource.updateSubscriptions(mergedSubscriptions) // Update the content when the subscription changes. var updateBasic = false var updatePremium = false acknowledgedSubscriptions?.forEach { subscription -> when (subscription.product) { Constants.BASIC_PRODUCT -> { updateBasic = true } Constants.PREMIUM_PRODUCT -> { updatePremium = true } } } if (updateBasic) { remoteDataSource.updateBasicContent() } else { // If we no longer own this content, clear it from the UI. _basicContent.emit(null) } if (updatePremium) { remoteDataSource.updatePremiumContent() } else { // If we no longer own this content, clear it from the UI. _premiumContent.emit(null) } } }
而这段重点在于他会拿local的purchase
跟刚刚register后的结果,呼叫acknowledgeRegisteredSubscriptionPurchaseTokens()
主要就是找出还没有被二次确认的订单,并且在此时再透过acknowledgeSubscription
对自己server 将该purchaseToken
进行确认,确认结果依样会回传SubscriptionStatus
这个data class,
/** * Acknowledge subscriptions that have been registered by the server * and update local data source. * Returns a list of acknowledged subscriptions. * */ private suspend fun acknowledgeRegisteredSubscriptionPurchaseTokens( remoteSubscriptions: List<SubscriptionStatus> ): List<SubscriptionStatus> { return remoteSubscriptions.map { sub -> if (!sub.isAcknowledged) { val acknowledgedSubs = sub.purchaseToken?.let { token -> sub.product?.let { product -> acknowledgeSubscription(product, token) } } acknowledgedSubs?.let { subList -> localDataSource.updateSubscriptions(subList) subList.map { sub.copy(isAcknowledged = true) } } ?: listOf(sub) } else { Log.d(TAG, "Subscription is already acknowledged") listOf(sub) } }.flatten() }
这时候返回到BillingRepoistory
会将所有结果整合,并且再次更新到localDb中当作cache资料
val mergedSubscriptions = mergeSubscriptionsAndPurchases( currentSubscriptions, acknowledgedSubscriptions, purchases )
然后于下方抓出acknowledgedSubscriptions
,也就是确认已经经过二次确认的商品,纪录商品更新了
acknowledgedSubscriptions?.forEach { subscription -> when (subscription.product) { Constants.BASIC_PRODUCT -> { updateBasic = true } Constants.PREMIUM_PRODUCT -> { updatePremium = true } } }
最后sample中,会再次跟他自己的server,去取得这个baseContent
的一些应该要显示的内容,譬如成功的图片网址
或者任何要显示的内容
,最后更新到UI上面进行显示
if (updateBasic) { remoteDataSource.updateBasicContent() } else { // If we no longer own this content, clear it from the UI. _basicContent.emit(null) } if (updatePremium) { remoteDataSource.updatePremiumContent() } else { // If we no longer own this content, clear it from the UI. _premiumContent.emit(null) }
大致上v6在这次的 acknowledge 调整会是这样,其他还有一些额外内容,有机会在更新啰