关于Google Billing Api V6 中的二次确认订单 acknowledge Subscription异动

在今年的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取得到purchaseToken后,必须先呼叫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 调整会是这样,其他还有一些额外内容,有机会在更新啰


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章