作者:業(yè)志陳
現(xiàn)如今,App 出海熱度不減,是很多公司和個(gè)人開(kāi)發(fā)者選擇的一個(gè)市場(chǎng)方向。App 為了實(shí)現(xiàn)盈利,除了接入廣告這種最常見(jiàn)的變現(xiàn)方式外,就是通過(guò)提供各類虛擬商品或者是會(huì)員服務(wù)來(lái)吸引用戶付費(fèi)了,此時(shí) Google Play 結(jié)算系統(tǒng)(Google Play’s billing system)就是 Android 端應(yīng)用必須使用到的一個(gè)支付渠道了
Google 對(duì) Google Play 結(jié)算系統(tǒng)的簡(jiǎn)介:Google Play’s billing system is a service that enables you to sell digital products and content in your Android app, whether you want to monetize through one-time purchases or offer subscriptions to your services. Google Play offers a full set of APIs for integration with both your Android app and your server backend that unlock the familiarity and safety of Google Play purchases for your users.
也就是說(shuō):Google Play 結(jié)算系統(tǒng)是一項(xiàng)可以讓我們?cè)?Android 應(yīng)用中銷售數(shù)字商品和內(nèi)容的服務(wù)。無(wú)論是要通過(guò)一次性購(gòu)買交易創(chuàng)收,還是要為用戶提供訂閱服務(wù),它都能幫我們搞定。Google Play 提供了一整套 API,可集成到 Android 應(yīng)用和服務(wù)器后端中,從而為用戶提供熟悉又安全的 Google Play 購(gòu)買交易服務(wù)
在最近的一年多時(shí)間里,我一直在負(fù)責(zé)一個(gè)海外項(xiàng)目的開(kāi)發(fā)工作,這個(gè)過(guò)程中也接入了 Google Play 結(jié)算系統(tǒng)。在剛開(kāi)始時(shí),由于對(duì)當(dāng)中的各個(gè)概念不夠了解,其整體支付流程又和國(guó)內(nèi)常用的各類支付服務(wù)相差挺大的,導(dǎo)致我走了不少的彎路
這里我就來(lái)寫一篇文章,對(duì) Google Play 結(jié)算系統(tǒng)進(jìn)行詳細(xì)介紹,希望對(duì)你有所幫助
一、概述
想要通過(guò) Google Play 結(jié)算系統(tǒng)向用戶展示并售賣商品,自然需要先創(chuàng)建商品,創(chuàng)建商品的方式有兩種:
- 在 Google Play Console 手動(dòng)創(chuàng)建
- 通過(guò) Google Play Developer API 以代碼的方式創(chuàng)建
在 Google Play 中創(chuàng)建的商品都屬于虛擬商品,每個(gè)商品代表的都是 App 給用戶提供的一種權(quán)益,而每個(gè)商品都包含一個(gè)唯一標(biāo)識(shí),也即 ProductId,我們?cè)跇I(yè)務(wù)上就需要根據(jù) ProductId 的命名規(guī)則來(lái)定義商品所代表的具體權(quán)益類型
每個(gè)商品又可以分為兩種類型:
- 一次性商品。用戶通過(guò)單次付費(fèi)獲得的商品,屬于買斷制,對(duì)應(yīng) Google Play 結(jié)算庫(kù)中的
BillingClient.ProductType.INAPP
- 訂閱型商品。用戶以固定周期不斷重復(fù)付費(fèi)的商品,屬于訂閱制,對(duì)應(yīng) Google Play 結(jié)算庫(kù)中的
BillingClient.ProductType.SUBS
當(dāng)用戶購(gòu)買了商品后,App 還需要對(duì)這筆訂單進(jìn)行核銷。處理流程和商品類型有關(guān),分為兩種:
- 確認(rèn)交易。不管購(gòu)買的商品是什么類型,App 都需要先對(duì)這筆交易進(jìn)行 確認(rèn),如果在限定的時(shí)間內(nèi)未完成確認(rèn),Google Play 就會(huì)自動(dòng)撤銷這筆交易并向用戶退款?!按_認(rèn)交易” 這個(gè)操作應(yīng)該是 Google Play 為了讓 App 確定已經(jīng)向用戶提供了權(quán)益,盡量避免出現(xiàn)用戶已付款但 App 沒(méi)有向用戶下發(fā)權(quán)益這種情況。確認(rèn)操作可以由服務(wù)端或者移動(dòng)端來(lái)實(shí)現(xiàn),對(duì)應(yīng)
acknowledgePurchase
操作 - 消耗商品。消耗商品針對(duì)的是一次性商品中的消耗型商品,也即對(duì)其執(zhí)行 消耗 操作。通過(guò)執(zhí)行消耗操作,使得用戶后續(xù)可以再次購(gòu)買此商品。消耗操作可以由服務(wù)端或者移動(dòng)端來(lái)實(shí)現(xiàn),對(duì)應(yīng)
consumePurchase
操作
二、一次性商品
一次性商品也稱為應(yīng)用內(nèi)商品,屬于一次性買斷的商品,具體又可以細(xì)分為兩種子類型:
- 消耗型商品。也即是說(shuō),此商品在購(gòu)買后可以被消耗,從而使得用戶可以重復(fù)購(gòu)買。例如,該商品可以用于表示游戲中的金幣,用戶在使用完金幣后該商品代表的權(quán)益就失效了,用戶需要再次購(gòu)買商品才能再次獲得金幣
- 非消耗型商品。也即是說(shuō),此商品在購(gòu)買后是不可消耗的,用戶可以永久獲得該商品代表的權(quán)益。例如,該商品可以用于表示某課程的觀看權(quán)益,用戶只要購(gòu)買商品后,就可以永久享有該課程的觀看權(quán)益
一次性商品到底屬于 消耗型 還是 非消耗型 都取決于 App 在業(yè)務(wù)上的定義,在 Google Play Console 中都統(tǒng)一將其稱為 應(yīng)用內(nèi)商品,在創(chuàng)建一次性商品時(shí)也沒(méi)有區(qū)分子類型的選項(xiàng)
假設(shè)我們對(duì)一件一次性商品在業(yè)務(wù)上的定義是消耗型的,那么就可以在適當(dāng)?shù)臅r(shí)候通過(guò)執(zhí)行 consumePurchase
來(lái)對(duì)其執(zhí)行 “消耗” 操作。例如,用戶通過(guò)購(gòu)買某個(gè)一次性商品獲得了游戲金幣,用戶在后續(xù)過(guò)程中使用這些金幣來(lái)購(gòu)買游戲道具,那么開(kāi)發(fā)者就需要同時(shí)執(zhí)行 consumePurchase
來(lái)消耗掉商品,從而使得該商品變?yōu)闊o(wú)效狀態(tài),這樣用戶后續(xù)也可以再次購(gòu)買此商品
而對(duì)于非消耗型商品,在業(yè)務(wù)上代表的是用戶可以永久享有的某個(gè)權(quán)益,只要買了該商品權(quán)益就不會(huì)丟失,因此用戶也不應(yīng)該再次購(gòu)買,自然也就不需要也不能執(zhí)行消耗操作了
三、訂閱型商品
訂閱型商品,也即需要用戶以固定周期定期進(jìn)行付費(fèi)的商品,在付費(fèi)周期內(nèi)用戶均能享有該商品代表的權(quán)益。最常見(jiàn)的應(yīng)用場(chǎng)景就是各類會(huì)員服務(wù):用戶按月付費(fèi),App 在每個(gè)訂閱周期內(nèi)向用戶提供會(huì)員獨(dú)有的功能,直至用戶取消訂閱
訂閱型商品包含四個(gè)比較重要的概念:
- 基礎(chǔ)方案
- 續(xù)訂類型
- 優(yōu)惠
- 定價(jià)階段
基礎(chǔ)方案
基礎(chǔ)方案,也稱為 BasePaln,每個(gè)訂閱型商品都必須包含一個(gè)或多個(gè)基礎(chǔ)方案才能讓用戶購(gòu)買
基礎(chǔ)方案就用于定義商品的售賣規(guī)則,包括結(jié)算周期、續(xù)訂類型、訂閱價(jià)格、優(yōu)惠策略等。例如,一個(gè)訂閱型商品可以同時(shí)提供 按月付費(fèi) 和 按年付費(fèi) 這兩個(gè)基礎(chǔ)方案供用戶選擇,每個(gè)周期分別設(shè)定不同的價(jià)格,用戶根據(jù)喜好來(lái)選擇不同的方案進(jìn)行訂閱
續(xù)訂類型
每個(gè)基礎(chǔ)方案均需要指定續(xù)訂類型,用于指定用戶的付費(fèi)方式
續(xù)訂類型分為兩種:
- 自動(dòng)續(xù)訂。在每個(gè)結(jié)算周期即將結(jié)束時(shí)主動(dòng)向用戶扣款,從而自動(dòng)延長(zhǎng)權(quán)益使用權(quán)的期限。付費(fèi)操作對(duì)于用戶來(lái)說(shuō)是被動(dòng)的
- 預(yù)付費(fèi)。不會(huì)自動(dòng)續(xù)訂和扣款,用戶需要通過(guò)主動(dòng)付款來(lái)推遲權(quán)益使用權(quán)的結(jié)束日期,以此保持不間斷地享有訂閱內(nèi)容。付費(fèi)操作對(duì)于用戶來(lái)說(shuō)是主動(dòng)的
優(yōu)惠
優(yōu)惠,也稱為 Offer,只有 自動(dòng)續(xù)訂型 的基礎(chǔ)方案才能設(shè)定優(yōu)惠
每個(gè)自動(dòng)續(xù)訂型的基礎(chǔ)方案可以同時(shí)設(shè)定多個(gè)優(yōu)惠,讓用戶可以在訂閱初期享受一定的價(jià)格折扣或者是直接就免費(fèi)使用,從而吸引用戶購(gòu)買
Offer 的類型分為三種,也即分為三種優(yōu)惠策略。例如,假設(shè)現(xiàn)在有一個(gè)按月訂閱的基礎(chǔ)方案,我們就可以為其添加以下三個(gè) Offer 供用戶選擇:
- 免費(fèi)試訂。用戶在前七天內(nèi)免費(fèi)試用,在七天后再正式進(jìn)行按月付費(fèi)
- 單次付款。用戶一次性預(yù)付三個(gè)月的訂閱費(fèi)用,總價(jià)享受七折折扣,三個(gè)月后再按原價(jià)進(jìn)行按月訂閱
- 周期性付款折扣。用戶還是按月訂閱,但前三個(gè)月每次付費(fèi)時(shí)均能享受八折折扣,三個(gè)月后再按原價(jià)進(jìn)行按月訂閱
價(jià)格階段
價(jià)格階段,也稱為 PricingPhases,可以看做是 Offer 的一個(gè)內(nèi)部屬性
由于一個(gè) Offer 可以同時(shí)包含多個(gè)優(yōu)惠策略,所以當(dāng)用戶在享用某個(gè) Offer 時(shí),其需要支出的價(jià)格就會(huì)隨時(shí)間發(fā)生多次變動(dòng),每個(gè)時(shí)間段分別對(duì)應(yīng)的不同的價(jià)格,PricingPhases 就用于表示 Offer 在每一個(gè)時(shí)間段的收費(fèi)規(guī)則
例如,某個(gè)按月自動(dòng)續(xù)訂的基礎(chǔ)方案包含一個(gè) Offer,此 Offer 包含一個(gè)七天免費(fèi)試訂的優(yōu)惠策略。那么,此 Offer 的價(jià)格階段就分別是:
- 用戶先享受七天的免費(fèi)試訂
- 七天后,用戶再按原價(jià)按月付費(fèi)
假如為這個(gè) Offer 再添加一個(gè) “折扣為七折,為期一個(gè)月的周期性付款” 的優(yōu)惠策略,此時(shí) Offer 的價(jià)格階段就變成了:
- 用戶先享受七天的免費(fèi)試訂
- 七天后,用戶按原價(jià)的七折進(jìn)行付費(fèi),獲得一個(gè)月的訂閱期
- 一個(gè)月后,用戶再按原價(jià)按月付費(fèi)
所以說(shuō),價(jià)格階段就決定了用戶在不同時(shí)間段下所需要支出的費(fèi)用,每個(gè) Offer 最多允許添加兩個(gè)價(jià)格階段,也即最多發(fā)生三次價(jià)格變動(dòng),用戶會(huì)按順序來(lái)接收價(jià)格變化
總結(jié)
Google Play 設(shè)定 BasePlan 和 Offer 的自由度很高。自動(dòng)續(xù)訂的 BasePlan 的付費(fèi)周期可以從一周到一年,預(yù)付費(fèi)的 BasePlan 的付費(fèi)周期可以從一天到一年。每種優(yōu)惠策略的優(yōu)惠周期和優(yōu)惠價(jià)也都可以很靈活地設(shè)定。我們可以通過(guò)設(shè)定多種不同的周期時(shí)長(zhǎng)和優(yōu)惠策略供用戶選擇,從而盡量提高用戶的付費(fèi)率
此外,每個(gè)訂閱型商品最多可以創(chuàng)建 250 個(gè)基礎(chǔ)方案和優(yōu)惠,但同時(shí)啟用的基礎(chǔ)方案和優(yōu)惠不能超過(guò) 50 個(gè),多出的基礎(chǔ)方案和優(yōu)惠必須處于草稿或未啟用狀態(tài)
四、Billing SDK
了解了以上的基礎(chǔ)概念后,再來(lái)看這些概念如何和 Billing SDK 對(duì)應(yīng)起來(lái)
本文所有的代碼示例使用的均是當(dāng)前 Google Play 結(jié)算系統(tǒng)在 Android 端最新版本的 SDK,且是協(xié)程版本,讀者需要對(duì)協(xié)程有一定了解
dependencies {
val billingVersion = "6.0.1"
implementation("com.android.billingclient:billing-ktx:$billingVersion")
}
整個(gè)支付流程可以總結(jié)為以下幾點(diǎn):
- 通過(guò) BillingClient 和 Google Play 建立連接,同時(shí)綁定用于回調(diào)支付結(jié)果的 PurchasesUpdatedListener 接口
- 通過(guò) BillingClient 查詢到本地化處理的商品信息,也即 ProductDetails,從而拿到 商品描述、基礎(chǔ)方案、價(jià)格信息、優(yōu)惠策略 等屬性
- 根據(jù)查到的 ProductDetails,向 BillingClient 發(fā)起支付請(qǐng)求,調(diào)起支付彈窗
- 在 PurchasesUpdatedListener 里拿到支付結(jié)果,判斷用戶的支付狀態(tài)
- 當(dāng)確定用戶支付成功后,根據(jù)商品類型擇機(jī)對(duì)商品進(jìn)行 確認(rèn) 或 消耗
BillingClient
BillingClient 是 Google Play 結(jié)算庫(kù)與 App 進(jìn)行通信的主接口,App 在執(zhí)行任何與支付相關(guān)的操作之前,都需要先通過(guò) BillingClient 和 Google Play 建立連接。在初始化 BillingClient 實(shí)例時(shí),需要同時(shí)綁定 PurchasesUpdatedListener,以便得到支付結(jié)果的回調(diào)通知。也正因?yàn)槿绱耍珹pp 在同一時(shí)間段最多只能保持一個(gè)活躍的 BillingClient 連接,以免同一個(gè)支付事件同時(shí)回調(diào)多個(gè) PurchasesUpdatedListener
private val purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->
}
private lateinit var billingClient: BillingClient
suspend fun startConnection(context: Context) {
billingClient = buildBillingClient(context = context, purchasesUpdatedListener)
startConnection(billingClient = mBillingClient)
}
private fun buildBillingClient(
context: Context,
listener: PurchasesUpdatedListener
): BillingClient {
return BillingClient.newBuilder(context)
.setListener(listener)
.enablePendingPurchases()
.build()
}
private suspend fun startConnection(billingClient: BillingClient): BillingResult? {
return withContext(context = Dispatchers.Default) {
if (billingClient.isReady) {
return@withContext null
}
return@withContext suspendCancellableCoroutine { continuation ->
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (!continuation.isCompleted) {
continuation.resume(value = billingResult)
}
}
override fun onBillingServiceDisconnected() {
if (!continuation.isCompleted) {
continuation.resume(value = null)
}
}
})
}
}
}
ProductDetails
ProductDetails 也即商品詳情,不管是一次性商品還是訂閱型商品,都通過(guò) ProductDetails 來(lái)承載具體的商品信息
查詢 ProductDetails 需要兩個(gè)查詢參數(shù):ProductId 和 商品類型,商品類型也即 一次性商品 INAPP 和 訂閱型商品 SUBS 兩種
private suspend fun queryProductDetails() {
//查詢一次性商品
queryProductDetails(
billingClient = mBillingClient,
productIdList = setOf("1", "2"),
productType = BillingClient.ProductType.INAPP
)
//查詢訂閱型商品
queryProductDetails(
billingClient = mBillingClient,
productIdList = setOf("1", "2"),
productType = BillingClient.ProductType.SUBS
)
}
private suspend fun queryProductDetails(
billingClient: BillingClient,
productIdList: Set<String>,
productType: String
): List<ProductDetails>? {
return withContext(context = Dispatchers.Default) {
if (!billingClient.isReady || productIdList.isEmpty()) {
return@withContext null
}
val productDetailParamsList = productIdList.map {
QueryProductDetailsParams
.Product
.newBuilder()
.setProductId(it)
.setProductType(productType)
.build()
}
val queryProductDetailsParams = QueryProductDetailsParams
.newBuilder()
.setProductList(productDetailParamsList)
.build()
val productDetailsResult = billingClient.queryProductDetails(queryProductDetailsParams)
productDetailsResult.productDetailsList
}
}
ProductDetails 的數(shù)據(jù)結(jié)構(gòu)如下所示,我們可以依靠這些信息來(lái)向用戶展示商品詳情。oneTimePurchaseOfferDetails 和 subscriptionOfferDetails 這兩個(gè)字段就分別用來(lái)承載一次性商品和訂閱型商品的價(jià)格信息
{
"productId": "",
"productType": "",
"title": "",
"name": "",
"description": "",
"oneTimePurchaseOfferDetails": {},
"subscriptionOfferDetails": []
}
oneTimePurchaseOfferDetails
oneTimePurchaseOfferDetails 對(duì)應(yīng)的是一次性商品的詳情,數(shù)據(jù)結(jié)構(gòu)比較簡(jiǎn)單,主要就是價(jià)格信息了
{
"priceAmountMicros": 548000000,
"priceCurrencyCode": "HKD",
"formattedPrice": "HK$548.00"
}
需要注意,Google Play 返回的價(jià)格信息都是做了本地化處理的,會(huì)自動(dòng)根據(jù)當(dāng)前設(shè)備的 Google Play 賬號(hào)所對(duì)應(yīng)的國(guó)家地區(qū)來(lái)返回詳情,所以商品的價(jià)格貨幣代號(hào) priceCurrencyCode
和格式化好的商品價(jià)格 formattedPrice
都會(huì)因?qū)嶋H情況而變化
subscriptionOfferDetails
subscriptionOfferDetails 對(duì)應(yīng)的是訂閱型商品的詳情
由于訂閱型商品是可以包含多個(gè) BasePlan 的,每個(gè) BasePlan 又可以包含多個(gè) Offer,所以 subscriptionOfferDetails 字段在 ProductDetails 中對(duì)應(yīng)的數(shù)據(jù)類型是 List<SubscriptionOfferDetails>
。每個(gè) SubscriptionOfferDetails 都對(duì)應(yīng)一個(gè) Offer,每個(gè) Offer 又關(guān)聯(lián)一個(gè) BasePlan,Google Play 以 Offer 為單位來(lái)返回價(jià)格信息
[
{
"basePlanId": "yearly",
"offerId": null,
"offerToken": "xxx",
"pricingPhases": {
"pricingPhaseList": [
{
"formattedPrice": "HK$469.00",
"priceAmountMicros": 469000000,
"priceCurrencyCode": "HKD",
"billingPeriod": "P1Y",
"billingCycleCount": 0,
"recurrenceMode": 1
}
]
}
},
{
"basePlanId": "yearly",
"offerId": "xxx",
"offerToken": "xxx",
"pricingPhases": {
"pricingPhaseList": [
{
"formattedPrice": "免費(fèi)",
"priceAmountMicros": 0,
"priceCurrencyCode": "HKD",
"billingPeriod": "P1W",
"billingCycleCount": 1,
"recurrenceMode": 2
},
{
"formattedPrice": "HK$469.00",
"priceAmountMicros": 469000000,
"priceCurrencyCode": "HKD",
"billingPeriod": "P1Y",
"billingCycleCount": 0,
"recurrenceMode": 1
}
]
}
}
]
上文有講到,Offer 是包含價(jià)格階段 PricingPhases 這個(gè)概念的,這個(gè)概念就體現(xiàn)在以上 Json 中,當(dāng)中就可以解讀出以下商品信息:
- 該商品包含一個(gè) Id 為 yearly 的 basePlan,一共包含兩個(gè) Offer
- offerToken 用于唯一標(biāo)識(shí)每一個(gè) Offer,具有唯一性
- billingPeriod 用于表示計(jì)費(fèi)周期,以 ISO 8601 格式來(lái)指定。例如,P1W 表示一周,P1Y 表示一年,P1M3D 表示一個(gè)月加三天
- billingCycleCount 用于表示計(jì)費(fèi)周期的周期數(shù)。例如,以上的第二個(gè) Offer 的第一個(gè) PricingPhases,就表示允許用戶免費(fèi)試用一周;假如 billingCycleCount 是 2,就表示允許用戶免費(fèi)試用兩周
- recurrenceMode 用于表示價(jià)格階段的重復(fù)模式,當(dāng)值為 1 或 3 時(shí),billingCycleCount 值都會(huì)是 0
- 值為 1 就表示將在無(wú)限的計(jì)費(fèi)周期內(nèi)重復(fù)進(jìn)行,除非用戶主動(dòng)取消
- 值為 2 就表示將在 billingCycleCount 指定的周期內(nèi)重復(fù)扣費(fèi)
- 值為 3 表示是一次性收費(fèi),不會(huì)重復(fù)
- 第一個(gè) Offer 的 offerId 為 null,說(shuō)明此 Offer 不包含實(shí)際的優(yōu)惠策略,代表的其實(shí)是 BasePlan 的原價(jià),所以 pricingPhaseList 也會(huì)只有一個(gè)值。且由于 billingPeriod 是 P1Y,說(shuō)明關(guān)聯(lián)的 BasePlan 的付費(fèi)周期是一年。選中此 Offer 后用戶就需要直接付 HK$469.00 的原價(jià)來(lái)進(jìn)行訂閱
- 第二個(gè) Offer 的 offerId 不為 null,說(shuō)明此 Offer 包含真實(shí)的優(yōu)惠策略,所以 pricingPhaseList 的大小就會(huì)大于一。該 Offer 允許用戶先免費(fèi)試用一周,然后再和第一個(gè) Offer 同樣的價(jià)格和周期來(lái)進(jìn)行訂閱
所以說(shuō),想要解讀出 BasePlan 的定價(jià)策略和 Offer 的優(yōu)惠策略,就需要結(jié)合所有字段來(lái)進(jìn)行解析。首先,不管我們?cè)趧?chuàng)建 BasePlan 時(shí)有沒(méi)有為其指定優(yōu)惠策略,Google Play 都會(huì)將 BasePlan 的原價(jià)視為一個(gè) Offer 并返回,這種情況下 Offer 也只會(huì)有一個(gè)定價(jià)階段。而對(duì)于真實(shí)的優(yōu)惠策略,其 offerId 是必須設(shè)定的,自然也就不會(huì)為 null,也會(huì)有最多三個(gè)定價(jià)階段。我們要區(qū)分出 “虛假的” Offer 和 "真實(shí)的” Offer。然后,再通過(guò) pricingPhases 來(lái)解析出 BasePlan 的訂閱周期和價(jià)格、Offer 的優(yōu)惠策略、Offer 的價(jià)格階段具體是如何設(shè)定的。這樣我們才能向用戶完整展示整個(gè)商品的價(jià)格信息
launchBillingFlow
launchBillingFlow 用于調(diào)起支付彈窗發(fā)起支付操作,根據(jù)商品類型,其調(diào)用方式分為兩種
假如要購(gòu)買的是一次性商品,支付參數(shù)僅需要 ProductDetails 即可
private suspend fun launchBilling(
activity: Activity,
billingClient: BillingClient,
productDetails: ProductDetails
): BillingResult {
return withContext(context = Dispatchers.Main.immediate) {
val productDetailsParams = BillingFlowParams
.ProductDetailsParams
.newBuilder()
.setProductDetails(productDetails)
.build()
val billingFlowParams = BillingFlowParams
.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
}
假如要購(gòu)買的是訂閱型商品,則需要同時(shí)傳遞 ProductDetails 和 offerToken
由于一個(gè)訂閱型商品可能同時(shí)包含多個(gè) BasePlan 和多個(gè) Offer,每個(gè) Offer 的優(yōu)惠策略又各不相同。因此 App 在發(fā)起支付操作時(shí),就需要通過(guò) offerToken 來(lái)標(biāo)明用戶想要購(gòu)買的到底是哪個(gè) BasePlan,選中的又是哪個(gè) Offer。而由于 Google Play 也會(huì)將 BasePlan 的原價(jià)視為一個(gè) Offer 并返回,所以我們是可以自主選擇要不要讓用戶享用優(yōu)惠的,自由度還是比較高的
private suspend fun launchBilling(
activity: Activity,
billingClient: BillingClient,
productDetails: ProductDetails,
offerToken: String
): BillingResult {
return withContext(context = Dispatchers.Main.immediate) {
val productDetailsParams = BillingFlowParams
.ProductDetailsParams
.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
val billingFlowParams = BillingFlowParams
.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
}
之后,我們?cè)?PurchasesUpdatedListener 回調(diào)里來(lái)獲取用戶的支付結(jié)果
假如用戶已支付成功,Purchase 就包含了此筆訂單的具體信息,包括 ProductId、OrderId、Quantity、PurchaseTime 等
private val purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
if (!purchases.isNullOrEmpty()) {
purchases.forEach {
when (it.purchaseState) {
Purchase.PurchaseState.PURCHASED -> {
//用戶支付成功
}
Purchase.PurchaseState.PENDING -> {
//用戶僅是預(yù)創(chuàng)建了訂單,還未真正付款
}
Purchase.PurchaseState.UNSPECIFIED_STATE -> {
//未知
}
}
}
}
}
BillingClient.BillingResponseCode.USER_CANCELED -> {
//用戶取消支付
}
else -> {
}
}
}
acknowledgePurchase
用戶支付成功后,就需要對(duì)訂單進(jìn)行確認(rèn)了,否則 Google Play 會(huì)在限定時(shí)間內(nèi)退款給用戶
private suspend fun acknowledgePurchase(
billingClient: BillingClient,
purchase: Purchase
): Boolean {
return withContext(context = Dispatchers.Default) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
return@withContext false
}
if (purchase.isAcknowledged) {
return@withContext true
}
if (!billingClient.isReady) {
return@withContext false
}
val acknowledgePurchaseParams = AcknowledgePurchaseParams
.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
val acknowledgePurchase = billingClient.acknowledgePurchase(acknowledgePurchaseParams)
acknowledgePurchase.responseCode == BillingClient.BillingResponseCode.OK
}
}
consumePurchase
如果用戶購(gòu)買的是消耗型的一次性商品,那么就需要根據(jù)實(shí)際業(yè)務(wù)擇機(jī)對(duì)訂單執(zhí)行消耗操作了
private suspend fun consumePurchase(
billingClient: BillingClient,
purchase: Purchase
): Boolean {
return withContext(context = Dispatchers.Default) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
return@withContext false
}
if (!billingClient.isReady) {
return@withContext false
}
val consumeParams = ConsumeParams
.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
val consumeResult = billingClient.consumePurchase(consumeParams)
consumeResult.billingResult.responseCode == BillingClient.BillingResponseCode.OK
}
}
五、鑒權(quán)
當(dāng)用戶購(gòu)買商品后,就需要來(lái)考慮如何對(duì)用戶進(jìn)行鑒權(quán)了。如果鑒權(quán)失敗或者是鑒權(quán)錯(cuò)了,不僅會(huì)給用戶帶來(lái)不良體驗(yàn),引來(lái)用戶投訴,也有可能會(huì)給項(xiàng)目帶來(lái)不可估量的資金損失
按照一般情況,App 在供用戶使用時(shí),App 都會(huì)為當(dāng)前用戶創(chuàng)建一個(gè)自己賬戶體系下的用戶身份,我們可以稱之為 appUser。當(dāng)用戶購(gòu)買商品后,這筆訂單也會(huì)和當(dāng)前設(shè)備付款的 Google Play 賬號(hào)綁定在一起,我們可以稱之為 gpUser
如此一來(lái),這筆訂單就會(huì)和兩個(gè)不同角度下的用戶產(chǎn)生關(guān)聯(lián)。這也就連鎖帶來(lái)一個(gè)問(wèn)題:商品代表的權(quán)益應(yīng)該掛載在哪個(gè)用戶的名下?appUser 還是 gpUser ?
這兩個(gè)選擇都各有優(yōu)缺點(diǎn)
掛載在 appUser 名下:
- 優(yōu)點(diǎn):用戶權(quán)益清晰明確,可以精準(zhǔn)隔離用戶的權(quán)益狀態(tài)
- 缺點(diǎn):在國(guó)外,以游客身份來(lái)購(gòu)買虛擬商品是很常見(jiàn)的情況,假如 App 只允許正式用戶(綁定了郵箱或者電話號(hào)碼)才能購(gòu)買商品的話,很有可能會(huì)流失大部分的潛在付費(fèi)用戶。因此,如果 appUser 是游客的話,當(dāng)用戶卸載應(yīng)用、更換或者重置設(shè)備后,就有可能導(dǎo)致已付費(fèi)的用戶再也找不回這筆訂單了
掛載在 gpUser 名下:
- 優(yōu)點(diǎn):即使用戶卸載應(yīng)用、更換或者重置設(shè)備,只要當(dāng)前設(shè)備登錄的就是付款時(shí)的 Google Play 賬號(hào),App 都能通過(guò) Billing SDK 的
queryPurchasesAsync
方法重新找回該賬號(hào)名下所有的訂單信息,不用擔(dān)心出現(xiàn)權(quán)益丟失的情況。同個(gè) Google Play 賬號(hào)在不同設(shè)備上也能共同享有 App 的權(quán)益,用戶體驗(yàn)是最好的 - 缺點(diǎn):App 是無(wú)法拿到 gpUser 的唯一身份標(biāo)識(shí)的,容易出現(xiàn)賬號(hào)倒賣的情況,多個(gè)用戶通過(guò)共享同一個(gè) Google Play 賬號(hào)來(lái)一起享有同一筆訂單的權(quán)益
所以說(shuō),App 需要根據(jù)自己的業(yè)務(wù)類型和用戶屬性,來(lái)決定是否要允許游客也能進(jìn)行購(gòu)買操作,用戶應(yīng)該以哪種維度來(lái)進(jìn)行身份鑒權(quán),當(dāng)發(fā)現(xiàn)同筆訂單在多臺(tái)設(shè)備上生效時(shí),又應(yīng)該如何避免資產(chǎn)損失
六、最后
本文主要是以移動(dòng)端的角度來(lái)進(jìn)行闡述,雖然 Google Play 結(jié)算系統(tǒng)也允許在沒(méi)有 App 后端服務(wù)參與的情況下就直接完成整個(gè)支付流程并完成用戶鑒權(quán),但為了安全性考慮,最好還是需要將訂單信息同步保存到服務(wù)端,并由服務(wù)端對(duì)訂單進(jìn)行校驗(yàn)后再?zèng)Q定是否要下發(fā)權(quán)益。此外,用戶是可以在不經(jīng)過(guò) App 的情況下,直接從 Google Play 中取消訂閱或者恢復(fù)訂閱,App 無(wú)法實(shí)時(shí)獲知該筆訂單的狀態(tài)變化,此時(shí) Google Play 也只會(huì)通過(guò) 開(kāi)發(fā)者實(shí)時(shí)通知 將這種變化通知給服務(wù)端,這種情況下也需要服務(wù)端的參與才能完整記錄下用戶的整個(gè)付費(fèi)狀態(tài)變化文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-702918.html
Android 學(xué)習(xí)筆錄
Android 性能優(yōu)化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學(xué)習(xí)筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內(nèi)含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源碼解析筆記:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知識(shí)體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集:https://qr18.cn/CgxrRy
Android 車載開(kāi)發(fā)崗位面試習(xí)題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-702918.html
到了這里,關(guān)于App 出海實(shí)踐:Google Play 結(jié)算系統(tǒng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!