用途
当用户已拥有有效的 Google Play 订阅,需要将其迁移到不同的 SKU(升级、降级、跨级或试用转换)时,使用此技能。RevenueCat 通过单一的 PurchaseParams 构建器暴露 Google 的替换模式,并在服务端解析 linkedPurchaseToken 链,因此你无需编写令牌链处理代码。
前置条件
在调用计划变更前,确认以下条件:
- 用户恰好拥有一个你打算替换的有效 Google Play 订阅。
- 你拥有来自 Purchases.sharedInstance.awaitCustomerInfo() 的新鲜 CustomerInfo,或来自最近监听器回调的缓存值。
- 你已从 offerings.current 解析出目标 Package(参见 fetch-offerings 技能)。
- 通过 RevenueCat SDK 在类路径上包含 Google Play Billing Library 7+。
如果用户没有有效订阅,跳过此技能。对于全新购买,请改用 make-purchase。
选择替换模式
GoogleReplacementMode 与 Google 的计费模式一一对应。根据用户意图选择:
| 场景 | 模式 | 计费效果 |
|---|---|---|
| 标准升级(月付转年付) | WITH_TIME_PRORATION | 立即切换,剩余时间抵扣 |
| 升级,保留现有计费日期 | CHARGE_PRORATED_PRICE | 立即切换,现在按比例收费 |
| 切换到预付费计划或从预付费计划切换 | CHARGE_FULL_PRICE | 立即切换,现在全额收费 |
| 在有效免费试用期间升级 | CHARGE_PRORATED_PRICE | 立即切换,现在按比例收费 |
| 降级(年付转月付) | DEFERRED | 切换在下次续订时生效 |
注意:
- 不要对试用升级默认使用 WITHOUT_PRORATION。WITHOUT_PRORATION 会立即应用新计划,但在下次续订前不收费,这会让用户获得未付费的高级访问权限。应使用 CHARGE_PRORATED_PRICE 立即收取升级费用。
- 如果不设置模式,PurchaseParams 默认使用 WITHOUT_PRORATION。每次都要显式设置模式。
- DEFERRED 仅对降级有效。Google 拒绝延迟升级。
执行
从 CustomerInfo 派生 currentProductId。硬编码 SKU 在用户跨计划迁移时会失效。
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
// activeSubscriptions 条目为 "productId:basePlanId",去掉基础计划后缀
val currentProductId = customerInfo.activeSubscriptions
.firstOrNull()
?.substringBefore(":")
?: return // 没有有效订阅,改为路由到 make-purchase
val newPackage = offerings.current
?.availablePackages
?.firstOrNull { it.identifier == "premium_annual_package" }
?: return
val params = PurchaseParams.Builder(activity, newPackage)
.googleProductChangeInfo(
GoogleProductChangeInfo(
oldProductId = currentProductId,
replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION,
)
)
.build()
try {
val result = Purchases.sharedInstance.awaitPurchase(params)
// result.customerInfo 反映新订阅
} catch (e: PurchasesTransactionException) {
if (!e.userCancelled) showError(e.error.message)
}
关于 oldProductId 的说明:
- 只传递订阅产品 ID。如果传递 "basic_monthly:monthly_plan",SDK 会为你去掉 :monthly_plan,但自行切分意图更清晰。
- CustomerInfo.activeSubscriptions 使用 productId:basePlanId 形式。substringBefore(":") 可得到正确值。
验证
挂起调用返回后,读取更新后的 CustomerInfo:
- customerInfo.activeSubscriptions 现在包含新的 productId:basePlanId。
- customerInfo.entitlements["pro"]?.isActive 在切换期间保持 true;不要根据 SKU 字符串来限制 UI。
- 对于 DEFERRED 模式,activeSubscriptions 在下次续订前仍报告旧产品。RevenueCat 在服务端跟踪待处理的切换,并在 Google 发送续订 RTDN 后翻转权益。
你无需编写 linkedPurchaseToken 遍历代码。RevenueCat 解析链,将旧令牌标记为已替换,并将两个令牌归属到同一 App User ID。客户端代码读取权益并信任它们。
常见错误
| 错误 | 修复 |
|---|---|
| 将 oldProductId 硬编码为常量 | 从 customerInfo.activeSubscriptions.firstOrNull()?.substringBefore(":") 派生 |
| 将 "productId:basePlanId" 作为 oldProductId 传递 | 用 substringBefore(":") 切掉基础计划 |
| 对试用升级使用 WITHOUT_PRORATION | 使用 CHARGE_PRORATED_PRICE 以便用户现在被收费 |
| 对升级使用 DEFERRED | DEFERRED 仅用于降级;Google 拒绝延迟升级 |
| 编写后端代码跟踪 linkedPurchaseToken | RevenueCat 在服务端执行此操作,删除该代码 |
| 读取 SKU 来决定 UI 状态 | 改为读取 entitlements[...]?.isActive |
限制
- 仅适用于 Android 平台上的 Google Play 订阅。
- 需要 Google Play Billing Library 7+。
- DEFERRED 模式仅对降级有效。
- 其他平台或购买类型暂未提供。