rc-purchase-flow

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Purchase Flow

购买流程

A RevenueCat purchase on Android is two lines of code. The SDK handles the billing params, the Play sheet, the purchase token round trip, server side verification, and acknowledgment. You decide which package to pass in and what to do with the returned entitlements.
在Android上通过RevenueCat实现内购只需两行代码。SDK会处理账单参数、Play支付界面、购买令牌往返、服务器端验证以及确认操作。你只需决定传入哪个套餐,以及如何处理返回的权限。

Phase 1: Understand what awaitPurchase() does

阶段1:理解awaitPurchase()的作用

One call to
Purchases.sharedInstance.awaitPurchase(params)
runs six steps under the hood. You do not write any of this.
StepWhat happens
1Builds
BillingFlowParams
with the correct
ProductDetailsParams
from your
Package
2Calls
BillingClient.launchBillingFlow()
against the activity you passed
3Suspends until the
PurchasesUpdatedListener
result arrives
4If
OK
, posts the Play purchase token to the RevenueCat backend
5Backend verifies via
purchases.subscriptionsv2.get
or
purchases.products.get
6SDK acknowledges (subs or non-consumables) or consumes (consumables) within the 3 day Google window
The call returns
PurchaseResult(storeTransaction, customerInfo)
. Retriable failures are retried automatically.
See the Purchase Flow chapter on revenuecat.com for the six-step diagram and the full picture.
调用一次
Purchases.sharedInstance.awaitPurchase(params)
会在后台执行六个步骤,你无需编写任何相关代码。
步骤操作内容
1从你的
Package
中构建带有正确
ProductDetailsParams
BillingFlowParams
2针对你传入的Activity调用
BillingClient.launchBillingFlow()
3暂停执行,直到
PurchasesUpdatedListener
返回结果
4如果结果为
OK
,将Play购买令牌发送至RevenueCat后端
5后端通过
purchases.subscriptionsv2.get
purchases.products.get
进行验证
6SDK会在Google规定的3天窗口期内确认(订阅或非消耗品)或消耗(消耗品)购买
该调用会返回
PurchaseResult(storeTransaction, customerInfo)
。可重试的失败会自动重试。
查看revenuecat.com上的购买流程章节获取六步流程图和完整说明。

Phase 2: Prepare the package

阶段2:准备套餐

You need a
Package
from offerings before you can purchase. Fetch offerings, let your UI pick one, and hold the activity.
kotlin
val offerings = Purchases.sharedInstance.awaitOfferings()
val pkg = offerings.current?.monthly ?: return
If you need a specific offer instead of the default, resolve a
SubscriptionOption
and pass that to
PurchaseParams.Builder
instead of the package.
kotlin
val option = pkg.product.subscriptionOptions
    ?.firstOrNull { it.tags.contains("promo_50_off") }
    ?: pkg.product.defaultOption
    ?: return
For EU personalized pricing, chain
.isPersonalizedPrice(true)
on the builder so Play shows the customized price notice.
在进行购买前,你需要从offerings中获取一个
Package
。获取offerings,让UI选择一个,并持有当前Activity。
kotlin
val offerings = Purchases.sharedInstance.awaitOfferings()
val pkg = offerings.current?.monthly ?: return
如果你需要特定优惠而非默认选项,解析一个
SubscriptionOption
并将其传入
PurchaseParams.Builder
,而不是传入套餐。
kotlin
val option = pkg.product.subscriptionOptions
    ?.firstOrNull { it.tags.contains("promo_50_off") }
    ?: pkg.product.defaultOption
    ?: return
对于欧盟个性化定价,在builder上链式调用
.isPersonalizedPrice(true)
,这样Play会显示自定义价格通知。

Phase 3: Execute the purchase

阶段3:执行购买

Build the params, await the purchase, read
customerInfo
to gate access, and handle the two expected error branches.
kotlin
try {
    val result = Purchases.sharedInstance.awaitPurchase(
        PurchaseParams.Builder(activity, pkg).build()
    )
    val customerInfo = result.customerInfo
    if (customerInfo.entitlements["pro"]?.isActive == true) {
        navigateToApp()
    }
} catch (e: PurchasesTransactionException) {
    when {
        e.userCancelled -> { /* backed out, do nothing */ }
        e.error.code == PurchasesErrorCode.ProductAlreadyPurchasedError ->
            showMessage("You already have this subscription")
        else -> showError(e.error.message)
    }
}
Rules for this block:
  • PurchaseResult
    is
    @Poko
    . Access fields as
    result.customerInfo
    and
    result.storeTransaction
    . Do not destructure with
    val (transaction, customerInfo) = result
    .
  • customerInfo.entitlements["<id>"]?.isActive == true
    is the gate. Do not check
    storeTransaction
    to decide access.
  • PurchasesTransactionException
    is the only exception type thrown by
    awaitPurchase
    . Catch it, branch on
    userCancelled
    first, then on
    e.error.code
    .
  • User cancellation is not an error to surface. Swallow it.
If you prefer callbacks over coroutines,
purchaseWith(params, onError, onSuccess)
is the equivalent entry point.
构建参数,等待购买完成,读取
customerInfo
以控制访问权限,并处理两种预期的错误分支。
kotlin
try {
    val result = Purchases.sharedInstance.awaitPurchase(
        PurchaseParams.Builder(activity, pkg).build()
    )
    val customerInfo = result.customerInfo
    if (customerInfo.entitlements["pro"]?.isActive == true) {
        navigateToApp()
    }
} catch (e: PurchasesTransactionException) {
    when {
        e.userCancelled -> { /* 用户退出,不做处理 */ }
        e.error.code == PurchasesErrorCode.ProductAlreadyPurchasedError ->
            showMessage("你已订阅该服务")
        else -> showError(e.error.message)
    }
}
此代码块的规则:
  • PurchaseResult
    @Poko
    类型。通过
    result.customerInfo
    result.storeTransaction
    访问字段,不要使用
    val (transaction, customerInfo) = result
    进行解构。
  • customerInfo.entitlements["<id>"]?.isActive == true
    是访问控制的判断依据。不要通过
    storeTransaction
    来决定访问权限。
  • awaitPurchase
    只会抛出
    PurchasesTransactionException
    类型的异常。捕获该异常后,先判断
    userCancelled
    ,再根据
    e.error.code
    分支处理。
  • 用户取消操作不属于需要提示的错误,直接忽略即可。
如果你更喜欢回调而非协程,
purchaseWith(params, onError, onSuccess)
是等效的入口方法。

Phase 4: Inspect StoreTransaction only if you need it

阶段4:仅在需要时查看StoreTransaction

result.storeTransaction
is available but usually unused.
customerInfo
is the source of truth for entitlements. If you need transaction level data for logging or your own backend:
FieldTypeNotes
orderId
String?
Null for restored purchases
purchaseToken
String
Raw Play purchase token
productIds
List<String>
Product IDs in the transaction
purchaseTime
Long
Epoch millis
type
ProductType
SUBS
or
INAPP
result.storeTransaction
是可用的,但通常无需使用。
customerInfo
是权限的唯一可信来源。如果你需要交易级数据用于日志记录或自有后端:
字段类型说明
orderId
String?
恢复的购买记录为Null
purchaseToken
String
原始Play购买令牌
productIds
List<String>
交易中的产品ID列表
purchaseTime
Long
时间戳(毫秒级)
type
ProductType
SUBS
(订阅)或
INAPP
(内购)

Phase 5: Restore on reinstall or device switch

阶段5:重新安装或切换设备时恢复购买

Restore runs
queryPurchasesAsync()
, posts everything found to RevenueCat, and returns the fresh
CustomerInfo
. Gate access the same way you do after a purchase.
kotlin
try {
    val customerInfo = Purchases.sharedInstance.awaitRestore()
    if (customerInfo.entitlements["pro"]?.isActive == true) {
        navigateToApp()
    } else {
        showMessage("No active purchases found")
    }
} catch (e: PurchasesException) {
    showError(e.error.message)
}
Note the exception type.
awaitRestore()
throws
PurchasesException
, not
PurchasesTransactionException
. There is no
userCancelled
flag to check because no billing sheet is shown.
恢复购买会执行
queryPurchasesAsync()
,将所有找到的购买记录发送至RevenueCat,并返回最新的
CustomerInfo
。访问控制方式与购买完成后相同。
kotlin
try {
    val customerInfo = Purchases.sharedInstance.awaitRestore()
    if (customerInfo.entitlements["pro"]?.isActive == true) {
        navigateToApp()
    } else {
        showMessage("未找到活跃的购买记录")
    }
} catch (e: PurchasesException) {
    showError(e.error.message)
}
注意异常类型。
awaitRestore()
抛出的是
PurchasesException
,而非
PurchasesTransactionException
。由于不会显示支付界面,因此没有
userCancelled
标志可供判断。

Exception types at a glance

异常类型一览

CallThrown exceptionHas
userCancelled
?
awaitPurchase(params)
PurchasesTransactionException
Yes
awaitRestore()
PurchasesException
No
调用方法抛出的异常是否包含
userCancelled
awaitPurchase(params)
PurchasesTransactionException
awaitRestore()
PurchasesException

References

参考资料