網(wǎng)站設(shè)計(jì)美工多少網(wǎng)絡(luò)推廣電話(huà)
作者:業(yè)志陳
現(xiàn)如今,App 出海熱度不減,是很多公司和個(gè)人開(kāi)發(fā)者選擇的一個(gè)市場(chǎng)方向。App 為了實(shí)現(xiàn)盈利,除了接入廣告這種最常見(jiàn)的變現(xiàn)方式外,就是通過(guò)提供各類(lèi)虛擬商品或者是會(huì)員服務(wù)來(lái)吸引用戶(hù)付費(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)用中銷(xiāo)售數(shù)字商品和內(nèi)容的服務(wù)。無(wú)論是要通過(guò)一次性購(gòu)買(mǎi)交易創(chuàng)收,還是要為用戶(hù)提供訂閱服務(wù),它都能幫我們搞定。Google Play 提供了一整套 API,可集成到 Android 應(yīng)用和服務(wù)器后端中,從而為用戶(hù)提供熟悉又安全的 Google Play 購(gòu)買(mǎi)交易服務(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)常用的各類(lèi)支付服務(wù)相差挺大的,導(dǎo)致我走了不少的彎路
這里我就來(lái)寫(xiě)一篇文章,對(duì) Google Play 結(jié)算系統(tǒng)進(jìn)行詳細(xì)介紹,希望對(duì)你有所幫助
一、概述
想要通過(guò) Google Play 結(jié)算系統(tǒng)向用戶(hù)展示并售賣(mài)商品,自然需要先創(chuàng)建商品,創(chuàng)建商品的方式有兩種:
- 在 Google Play Console 手動(dòng)創(chuàng)建
- 通過(guò) Google Play Developer API 以代碼的方式創(chuàng)建
在 Google Play 中創(chuàng)建的商品都屬于虛擬商品,每個(gè)商品代表的都是 App 給用戶(hù)提供的一種權(quán)益,而每個(gè)商品都包含一個(gè)唯一標(biāo)識(shí),也即 ProductId,我們?cè)跇I(yè)務(wù)上就需要根據(jù) ProductId 的命名規(guī)則來(lái)定義商品所代表的具體權(quán)益類(lèi)型
每個(gè)商品又可以分為兩種類(lèi)型:
- 一次性商品。用戶(hù)通過(guò)單次付費(fèi)獲得的商品,屬于買(mǎi)斷制,對(duì)應(yīng) Google Play 結(jié)算庫(kù)中的
BillingClient.ProductType.INAPP
- 訂閱型商品。用戶(hù)以固定周期不斷重復(fù)付費(fèi)的商品,屬于訂閱制,對(duì)應(yīng) Google Play 結(jié)算庫(kù)中的
BillingClient.ProductType.SUBS
當(dāng)用戶(hù)購(gòu)買(mǎi)了商品后,App 還需要對(duì)這筆訂單進(jìn)行核銷(xiāo)。處理流程和商品類(lèi)型有關(guān),分為兩種:
- 確認(rèn)交易。不管購(gòu)買(mǎi)的商品是什么類(lèi)型,App 都需要先對(duì)這筆交易進(jìn)行 確認(rèn),如果在限定的時(shí)間內(nèi)未完成確認(rèn),Google Play 就會(huì)自動(dòng)撤銷(xiāo)這筆交易并向用戶(hù)退款?!按_認(rèn)交易” 這個(gè)操作應(yīng)該是 Google Play 為了讓 App 確定已經(jīng)向用戶(hù)提供了權(quán)益,盡量避免出現(xiàn)用戶(hù)已付款但 App 沒(méi)有向用戶(hù)下發(fā)權(quán)益這種情況。確認(rèn)操作可以由服務(wù)端或者移動(dòng)端來(lái)實(shí)現(xiàn),對(duì)應(yīng)
acknowledgePurchase
操作 - 消耗商品。消耗商品針對(duì)的是一次性商品中的消耗型商品,也即對(duì)其執(zhí)行 消耗 操作。通過(guò)執(zhí)行消耗操作,使得用戶(hù)后續(xù)可以再次購(gòu)買(mǎi)此商品。消耗操作可以由服務(wù)端或者移動(dòng)端來(lái)實(shí)現(xiàn),對(duì)應(yīng)
consumePurchase
操作
二、一次性商品
一次性商品也稱(chēng)為應(yīng)用內(nèi)商品,屬于一次性買(mǎi)斷的商品,具體又可以細(xì)分為兩種子類(lèi)型:
- 消耗型商品。也即是說(shuō),此商品在購(gòu)買(mǎi)后可以被消耗,從而使得用戶(hù)可以重復(fù)購(gòu)買(mǎi)。例如,該商品可以用于表示游戲中的金幣,用戶(hù)在使用完金幣后該商品代表的權(quán)益就失效了,用戶(hù)需要再次購(gòu)買(mǎi)商品才能再次獲得金幣
- 非消耗型商品。也即是說(shuō),此商品在購(gòu)買(mǎi)后是不可消耗的,用戶(hù)可以永久獲得該商品代表的權(quán)益。例如,該商品可以用于表示某課程的觀(guān)看權(quán)益,用戶(hù)只要購(gòu)買(mǎi)商品后,就可以永久享有該課程的觀(guān)看權(quán)益
一次性商品到底屬于 消耗型 還是 非消耗型 都取決于 App 在業(yè)務(wù)上的定義,在 Google Play Console 中都統(tǒng)一將其稱(chēng)為 應(yīng)用內(nèi)商品,在創(chuàng)建一次性商品時(shí)也沒(méi)有區(qū)分子類(lèi)型的選項(xiàng)
假設(shè)我們對(duì)一件一次性商品在業(yè)務(wù)上的定義是消耗型的,那么就可以在適當(dāng)?shù)臅r(shí)候通過(guò)執(zhí)行 consumePurchase
來(lái)對(duì)其執(zhí)行 “消耗” 操作。例如,用戶(hù)通過(guò)購(gòu)買(mǎi)某個(gè)一次性商品獲得了游戲金幣,用戶(hù)在后續(xù)過(guò)程中使用這些金幣來(lái)購(gòu)買(mǎi)游戲道具,那么開(kāi)發(fā)者就需要同時(shí)執(zhí)行 consumePurchase
來(lái)消耗掉商品,從而使得該商品變?yōu)闊o(wú)效狀態(tài),這樣用戶(hù)后續(xù)也可以再次購(gòu)買(mǎi)此商品
而對(duì)于非消耗型商品,在業(yè)務(wù)上代表的是用戶(hù)可以永久享有的某個(gè)權(quán)益,只要買(mǎi)了該商品權(quán)益就不會(huì)丟失,因此用戶(hù)也不應(yīng)該再次購(gòu)買(mǎi),自然也就不需要也不能執(zhí)行消耗操作了
三、訂閱型商品
訂閱型商品,也即需要用戶(hù)以固定周期定期進(jìn)行付費(fèi)的商品,在付費(fèi)周期內(nèi)用戶(hù)均能享有該商品代表的權(quán)益。最常見(jiàn)的應(yīng)用場(chǎng)景就是各類(lèi)會(huì)員服務(wù):用戶(hù)按月付費(fèi),App 在每個(gè)訂閱周期內(nèi)向用戶(hù)提供會(huì)員獨(dú)有的功能,直至用戶(hù)取消訂閱
訂閱型商品包含四個(gè)比較重要的概念:
- 基礎(chǔ)方案
- 續(xù)訂類(lèi)型
- 優(yōu)惠
- 定價(jià)階段
基礎(chǔ)方案
基礎(chǔ)方案,也稱(chēng)為 BasePaln,每個(gè)訂閱型商品都必須包含一個(gè)或多個(gè)基礎(chǔ)方案才能讓用戶(hù)購(gòu)買(mǎi)
基礎(chǔ)方案就用于定義商品的售賣(mài)規(guī)則,包括結(jié)算周期、續(xù)訂類(lèi)型、訂閱價(jià)格、優(yōu)惠策略等。例如,一個(gè)訂閱型商品可以同時(shí)提供 按月付費(fèi) 和 按年付費(fèi) 這兩個(gè)基礎(chǔ)方案供用戶(hù)選擇,每個(gè)周期分別設(shè)定不同的價(jià)格,用戶(hù)根據(jù)喜好來(lái)選擇不同的方案進(jìn)行訂閱
續(xù)訂類(lèi)型
每個(gè)基礎(chǔ)方案均需要指定續(xù)訂類(lèi)型,用于指定用戶(hù)的付費(fèi)方式
續(xù)訂類(lèi)型分為兩種:
- 自動(dòng)續(xù)訂。在每個(gè)結(jié)算周期即將結(jié)束時(shí)主動(dòng)向用戶(hù)扣款,從而自動(dòng)延長(zhǎng)權(quán)益使用權(quán)的期限。付費(fèi)操作對(duì)于用戶(hù)來(lái)說(shuō)是被動(dòng)的
- 預(yù)付費(fèi)。不會(huì)自動(dòng)續(xù)訂和扣款,用戶(hù)需要通過(guò)主動(dòng)付款來(lái)推遲權(quán)益使用權(quán)的結(jié)束日期,以此保持不間斷地享有訂閱內(nèi)容。付費(fèi)操作對(duì)于用戶(hù)來(lái)說(shuō)是主動(dòng)的
優(yōu)惠
優(yōu)惠,也稱(chēng)為 Offer,只有 自動(dòng)續(xù)訂型 的基礎(chǔ)方案才能設(shè)定優(yōu)惠
每個(gè)自動(dòng)續(xù)訂型的基礎(chǔ)方案可以同時(shí)設(shè)定多個(gè)優(yōu)惠,讓用戶(hù)可以在訂閱初期享受一定的價(jià)格折扣或者是直接就免費(fèi)使用,從而吸引用戶(hù)購(gòu)買(mǎi)
Offer 的類(lèi)型分為三種,也即分為三種優(yōu)惠策略。例如,假設(shè)現(xiàn)在有一個(gè)按月訂閱的基礎(chǔ)方案,我們就可以為其添加以下三個(gè) Offer 供用戶(hù)選擇:
- 免費(fèi)試訂。用戶(hù)在前七天內(nèi)免費(fèi)試用,在七天后再正式進(jìn)行按月付費(fèi)
- 單次付款。用戶(hù)一次性預(yù)付三個(gè)月的訂閱費(fèi)用,總價(jià)享受七折折扣,三個(gè)月后再按原價(jià)進(jìn)行按月訂閱
- 周期性付款折扣。用戶(hù)還是按月訂閱,但前三個(gè)月每次付費(fèi)時(shí)均能享受八折折扣,三個(gè)月后再按原價(jià)進(jìn)行按月訂閱
價(jià)格階段
價(jià)格階段,也稱(chēng)為 PricingPhases,可以看做是 Offer 的一個(gè)內(nèi)部屬性
由于一個(gè) Offer 可以同時(shí)包含多個(gè)優(yōu)惠策略,所以當(dāng)用戶(hù)在享用某個(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à)格階段就分別是:
- 用戶(hù)先享受七天的免費(fèi)試訂
- 七天后,用戶(hù)再按原價(jià)按月付費(fèi)
假如為這個(gè) Offer 再添加一個(gè) “折扣為七折,為期一個(gè)月的周期性付款” 的優(yōu)惠策略,此時(shí) Offer 的價(jià)格階段就變成了:
- 用戶(hù)先享受七天的免費(fèi)試訂
- 七天后,用戶(hù)按原價(jià)的七折進(jìn)行付費(fèi),獲得一個(gè)月的訂閱期
- 一個(gè)月后,用戶(hù)再按原價(jià)按月付費(fèi)
所以說(shuō),價(jià)格階段就決定了用戶(hù)在不同時(shí)間段下所需要支出的費(fèi)用,每個(gè) Offer 最多允許添加兩個(gè)價(jià)格階段,也即最多發(fā)生三次價(jià)格變動(dòng),用戶(hù)會(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)惠策略供用戶(hù)選擇,從而盡量提高用戶(hù)的付費(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 查詢(xún)到本地化處理的商品信息,也即 ProductDetails,從而拿到 商品描述、基礎(chǔ)方案、價(jià)格信息、優(yōu)惠策略 等屬性
- 根據(jù)查到的 ProductDetails,向 BillingClient 發(fā)起支付請(qǐng)求,調(diào)起支付彈窗
- 在 PurchasesUpdatedListener 里拿到支付結(jié)果,判斷用戶(hù)的支付狀態(tài)
- 當(dāng)確定用戶(hù)支付成功后,根據(jù)商品類(lèi)型擇機(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)槿绱?#xff0c;App 在同一時(shí)間段最多只能保持一個(gè)活躍的 BillingClient 連接,以免同一個(gè)支付事件同時(shí)回調(diào)多個(gè) PurchasesUpdatedListener
private val purchasesUpdatedListener =PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->}private lateinit var billingClient: BillingClientsuspend 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)承載具體的商品信息
查詢(xún) ProductDetails 需要兩個(gè)查詢(xún)參數(shù):ProductId 和 商品類(lèi)型,商品類(lèi)型也即 一次性商品 INAPP 和 訂閱型商品 SUBS 兩種
private suspend fun queryProductDetails() {//查詢(xún)一次性商品queryProductDetails(billingClient = mBillingClient,productIdList = setOf("1", "2"),productType = BillingClient.ProductType.INAPP)//查詢(xún)訂閱型商品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)向用戶(hù)展示商品詳情。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ù)類(lèi)型是 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,就表示允許用戶(hù)免費(fèi)試用一周;假如 billingCycleCount 是 2,就表示允許用戶(hù)免費(fèi)試用兩周
- recurrenceMode 用于表示價(jià)格階段的重復(fù)模式,當(dāng)值為 1 或 3 時(shí),billingCycleCount 值都會(huì)是 0
- 值為 1 就表示將在無(wú)限的計(jì)費(fèi)周期內(nèi)重復(fù)進(jìn)行,除非用戶(hù)主動(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 后用戶(hù)就需要直接付 HK$469.00 的原價(jià)來(lái)進(jìn)行訂閱
- 第二個(gè) Offer 的 offerId 不為 null,說(shuō)明此 Offer 包含真實(shí)的優(yōu)惠策略,所以 pricingPhaseList 的大小就會(huì)大于一。該 Offer 允許用戶(hù)先免費(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è)定的。這樣我們才能向用戶(hù)完整展示整個(gè)商品的價(jià)格信息
launchBillingFlow
launchBillingFlow 用于調(diào)起支付彈窗發(fā)起支付操作,根據(jù)商品類(lèi)型,其調(diào)用方式分為兩種
假如要購(gòu)買(mǎi)的是一次性商品,支付參數(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)買(mǎi)的是訂閱型商品,則需要同時(shí)傳遞 ProductDetails 和 offerToken
由于一個(gè)訂閱型商品可能同時(shí)包含多個(gè) BasePlan 和多個(gè) Offer,每個(gè) Offer 的優(yōu)惠策略又各不相同。因此 App 在發(fā)起支付操作時(shí),就需要通過(guò) offerToken 來(lái)標(biāo)明用戶(hù)想要購(gòu)買(mǎi)的到底是哪個(gè) BasePlan,選中的又是哪個(gè) Offer。而由于 Google Play 也會(huì)將 BasePlan 的原價(jià)視為一個(gè) Offer 并返回,所以我們是可以自主選擇要不要讓用戶(hù)享用優(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)獲取用戶(hù)的支付結(jié)果
假如用戶(hù)已支付成功,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 -> {//用戶(hù)支付成功}Purchase.PurchaseState.PENDING -> {//用戶(hù)僅是預(yù)創(chuàng)建了訂單,還未真正付款}Purchase.PurchaseState.UNSPECIFIED_STATE -> {//未知}}}}}BillingClient.BillingResponseCode.USER_CANCELED -> {//用戶(hù)取消支付}else -> {}}}
acknowledgePurchase
用戶(hù)支付成功后,就需要對(duì)訂單進(jìn)行確認(rèn)了,否則 Google Play 會(huì)在限定時(shí)間內(nèi)退款給用戶(hù)
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
如果用戶(hù)購(gòu)買(mǎi)的是消耗型的一次性商品,那么就需要根據(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)用戶(hù)購(gòu)買(mǎi)商品后,就需要來(lái)考慮如何對(duì)用戶(hù)進(jìn)行鑒權(quán)了。如果鑒權(quán)失敗或者是鑒權(quán)錯(cuò)了,不僅會(huì)給用戶(hù)帶來(lái)不良體驗(yàn),引來(lái)用戶(hù)投訴,也有可能會(huì)給項(xiàng)目帶來(lái)不可估量的資金損失
按照一般情況,App 在供用戶(hù)使用時(shí),App 都會(huì)為當(dāng)前用戶(hù)創(chuàng)建一個(gè)自己賬戶(hù)體系下的用戶(hù)身份,我們可以稱(chēng)之為 appUser。當(dāng)用戶(hù)購(gòu)買(mǎi)商品后,這筆訂單也會(huì)和當(dāng)前設(shè)備付款的 Google Play 賬號(hào)綁定在一起,我們可以稱(chēng)之為 gpUser
如此一來(lái),這筆訂單就會(huì)和兩個(gè)不同角度下的用戶(hù)產(chǎn)生關(guān)聯(lián)。這也就連鎖帶來(lái)一個(gè)問(wèn)題:商品代表的權(quán)益應(yīng)該掛載在哪個(gè)用戶(hù)的名下?appUser 還是 gpUser ?
這兩個(gè)選擇都各有優(yōu)缺點(diǎn)
掛載在 appUser 名下:
- 優(yōu)點(diǎn):用戶(hù)權(quán)益清晰明確,可以精準(zhǔn)隔離用戶(hù)的權(quán)益狀態(tài)
- 缺點(diǎn):在國(guó)外,以游客身份來(lái)購(gòu)買(mǎi)虛擬商品是很常見(jiàn)的情況,假如 App 只允許正式用戶(hù)(綁定了郵箱或者電話(huà)號(hào)碼)才能購(gòu)買(mǎi)商品的話(huà),很有可能會(huì)流失大部分的潛在付費(fèi)用戶(hù)。因此,如果 appUser 是游客的話(huà),當(dāng)用戶(hù)卸載應(yīng)用、更換或者重置設(shè)備后,就有可能導(dǎo)致已付費(fèi)的用戶(hù)再也找不回這筆訂單了
掛載在 gpUser 名下:
- 優(yōu)點(diǎn):即使用戶(hù)卸載應(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)益,用戶(hù)體驗(yàn)是最好的 - 缺點(diǎn):App 是無(wú)法拿到 gpUser 的唯一身份標(biāo)識(shí)的,容易出現(xiàn)賬號(hào)倒賣(mài)的情況,多個(gè)用戶(hù)通過(guò)共享同一個(gè) Google Play 賬號(hào)來(lái)一起享有同一筆訂單的權(quán)益
所以說(shuō),App 需要根據(jù)自己的業(yè)務(wù)類(lèi)型和用戶(hù)屬性,來(lái)決定是否要允許游客也能進(jìn)行購(gòu)買(mǎi)操作,用戶(hù)應(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è)支付流程并完成用戶(hù)鑒權(quán),但為了安全性考慮,最好還是需要將訂單信息同步保存到服務(wù)端,并由服務(wù)端對(duì)訂單進(jìn)行校驗(yàn)后再?zèng)Q定是否要下發(fā)權(quán)益。此外,用戶(hù)是可以在不經(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ù)端的參與才能完整記錄下用戶(hù)的整個(gè)付費(fèi)狀態(tài)變化
Android 學(xué)習(xí)筆錄
Android 性能優(yōu)化篇:https://qr18.cn/FVlo89
Android 車(chē)載篇: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 車(chē)載開(kāi)發(fā)崗位面試習(xí)題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap