rc-subscriptions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Subscriptions on Android with RevenueCat

在Android上使用RevenueCat实现订阅功能

Google Play exposes subscriptions through a three tier hierarchy: Subscription, Base Plan, and Offer. RevenueCat wraps that hierarchy in a flatter model you configure from the dashboard: Offerings, Packages, and SubscriptionOptions. You fetch an Offering, pick a Package, and in most flows let the SDK choose the right SubscriptionOption for you.
Google Play 通过三层层级结构展示订阅:Subscription、Base Plan和Offer。RevenueCat 将该层级结构封装为一个更扁平化的模型,你可以在控制台中配置:Offerings、Packages和SubscriptionOptions。你只需获取一个Offering,选择一个Package,在大多数流程中让SDK为你选择合适的SubscriptionOption即可。

Phase 1: Understand

阶段1:理解概念

The mapping from Google Play to RevenueCat:
Google PlayRevenueCat
Subscription (product ID)
StoreProduct
Base Plan
SubscriptionOption
(base plan)
Offer
SubscriptionOption
(offer)
Group of base plans grouped in dashboard
Package
inside an
Offering
Key types you will touch:
  • Offering
    : a dashboard configured group of
    Package
    objects.
    offerings.current
    is the one you show by default.
  • Package
    : a purchasable slot (monthly, annual, weekly, custom). Exposes a
    product: StoreProduct
    .
  • StoreProduct
    : the Google Play subscription product. Has
    subscriptionOptions: List<SubscriptionOption>?
    and a
    defaultOption
    .
  • SubscriptionOption
    : either a base plan or an offer. Has
    pricingPhases
    ,
    tags
    , and an
    id
    .
  • PricingPhase
    : one billing segment (intro trial, intro price, or recurring). Has
    billingPeriod
    ,
    price
    ,
    offerPaymentMode
    , and
    recurrenceMode
    .
See the Subscriptions chapter on revenuecat.com for the object model diagram showing the full Offerings hierarchy alongside the CustomerInfo hierarchy used for entitlement checks.
Google Play与RevenueCat的对象映射关系:
Google PlayRevenueCat
Subscription(产品ID)
StoreProduct
Base Plan
SubscriptionOption
(基础方案)
Offer
SubscriptionOption
(优惠活动)
控制台中分组的基础方案集合
Offering
下的
Package
你会接触到的核心类型:
  • Offering
    :在控制台配置的
    Package
    对象组。
    offerings.current
    是默认展示的对象。
  • Package
    :可购买的套餐(月度、年度、周度、自定义)。包含
    product: StoreProduct
    属性。
  • StoreProduct
    :Google Play订阅产品。包含
    subscriptionOptions: List<SubscriptionOption>?
    defaultOption
    属性。
  • SubscriptionOption
    :既可以是基础方案,也可以是优惠活动。包含
    pricingPhases
    tags
    id
    属性。
  • PricingPhase
    :单个计费阶段(试用期、入门价格或定期计费)。包含
    billingPeriod
    price
    offerPaymentMode
    recurrenceMode
    属性。
查看revenuecat.com上的订阅章节,其中的对象模型图展示了完整的Offerings层级结构,以及用于权限校验的CustomerInfo层级结构。

Phase 2: Plan

阶段2:规划实现

Before you write code, map your paywall to the object model. Answer these three questions.
在编写代码之前,将你的付费墙与对象模型进行映射。回答以下三个问题。

2.1 Which Offering drives the paywall?

2.1 哪个Offering驱动付费墙?

  • Default paywall: use
    offerings.current
    . This is the Offering marked current in the dashboard and is the standard choice.
  • Experiment or segment specific paywall: fetch
    offerings.all["experiment-a"]
    . You keep the dashboard in charge of which products appear, so no app update ships when the catalog changes.
  • 默认付费墙:使用
    offerings.current
    。这是在控制台中标记为当前的Offering,是标准选择。
  • 实验或细分用户专属付费墙:获取
    offerings.all["experiment-a"]
    。由控制台控制显示哪些产品,因此产品目录变更时无需发布应用更新。

2.2 Which Packages do you show?

2.2 你要展示哪些Packages?

Two access patterns, pick the one that matches your layout:
PatternAPIWhen to use
Named slots
offering.monthly
,
offering.annual
,
offering.weekly
Fixed paywall with known durations
Iteration
offering.availablePackages
Dynamic layout, unknown durations, or custom package types
Standard
PackageType
values:
MONTHLY
,
ANNUAL
,
WEEKLY
,
TWO_MONTH
,
THREE_MONTH
,
SIX_MONTH
,
LIFETIME
. Anything else is
PackageType.CUSTOM
.
有两种访问模式,选择与你的布局匹配的模式:
模式API使用场景
命名套餐
offering.monthly
,
offering.annual
,
offering.weekly
固定付费墙,时长已知
迭代遍历
offering.availablePackages
动态布局、时长未知或自定义套餐类型
标准
PackageType
值包括:
MONTHLY
ANNUAL
WEEKLY
TWO_MONTH
THREE_MONTH
SIX_MONTH
LIFETIME
。其他类型均为
PackageType.CUSTOM

2.3 Does the paywall need a specific offer, or is the default fine?

2.3 付费墙是否需要特定优惠活动,还是默认即可?

SituationWhat to pass to
PurchaseParams
Standard paywall, user gets best eligible offer automatically
Package
You need a specific offer (win back, promo, tag selected)
SubscriptionOption
The SDK's
defaultOption
logic:
  1. Filters out options tagged
    "rc-ignore-offer"
    or
    "rc-customer-center"
    .
  2. Picks the option with the longest free trial or the cheapest first phase.
  3. Falls back to the base plan if no offer qualifies.
Trial eligibility is not filtered by the SDK. Google Play only returns offers the user is eligible for, so if a user already consumed a free trial, that option simply will not appear in
subscriptionOptions
and the base plan becomes the default.
场景传递给
PurchaseParams
的内容
标准付费墙,自动为用户选择最佳符合条件的优惠
Package
需要特定优惠(赢回用户、促销、标签选中的优惠)
SubscriptionOption
SDK的
defaultOption
逻辑:
  1. 过滤掉标记为
    "rc-ignore-offer"
    "rc-customer-center"
    的选项。
  2. 选择免费试用时长最长或第一阶段价格最低的选项。
  3. 如果没有符合条件的优惠,则回退到基础方案。
SDK不会过滤试用资格。Google Play仅返回用户符合条件的优惠,因此如果用户已经使用过免费试用,该选项将不会出现在
subscriptionOptions
中,基础方案将成为默认选项。

Phase 3: Execute

阶段3:代码实现

3.1 Pull Offerings and pick a Package

3.1 获取Offerings并选择Package

kotlin
val offerings = Purchases.sharedInstance.awaitOfferings()
val offering = offerings.current ?: return
val monthly = offering.monthly ?: return
val product = monthly.product
val price = product.price.formatted
val period = product.period?.iso8601 // "P1M", "P1Y", null for one time
For a dynamic list:
kotlin
for (pkg in offering.availablePackages) {
    render(pkg.product.title, pkg.product.price.formatted, pkg.packageType)
}
kotlin
val offerings = Purchases.sharedInstance.awaitOfferings()
val offering = offerings.current ?: return
val monthly = offering.monthly ?: return
val product = monthly.product
val price = product.price.formatted
val period = product.period?.iso8601 // "P1M", "P1Y", null for one time
对于动态列表:
kotlin
for (pkg in offering.availablePackages) {
    render(pkg.product.title, pkg.product.price.formatted, pkg.packageType)
}

3.2 Purchase with the default option

3.2 使用默认选项进行购买

When the paywall shows a Package and you want the SDK to pick the best offer, pass the Package directly.
kotlin
val params = PurchaseParams.Builder(activity, monthly).build()
val result = Purchases.sharedInstance.awaitPurchase(params)
当付费墙展示Package,且你希望SDK选择最佳优惠时,直接传递Package即可。
kotlin
val params = PurchaseParams.Builder(activity, monthly).build()
val result = Purchases.sharedInstance.awaitPurchase(params)

3.3 Drill into
subscriptionOptions
for a specific offer

3.3 深入
subscriptionOptions
获取特定优惠

Use this when the paywall targets an offer by tag or offer ID, for example a win back offer.
kotlin
val product = offering.monthly?.product ?: return
val winBack = product.subscriptionOptions
    ?.firstOrNull { it.tags.contains("win-back") }
val option = winBack ?: product.defaultOption ?: return
val params = PurchaseParams.Builder(activity, option).build()
Always fall back to
defaultOption
so the paywall still works when the targeted offer is absent (for example, the user is not eligible).
当付费墙通过标签或优惠ID定位特定优惠时(例如赢回用户的优惠),使用此方法。
kotlin
val product = offering.monthly?.product ?: return
val winBack = product.subscriptionOptions
    ?.firstOrNull { it.tags.contains("win-back") }
val option = winBack ?: product.defaultOption ?: return
val params = PurchaseParams.Builder(activity, option).build()
始终回退到
defaultOption
,以便当目标优惠不存在时(例如用户不符合资格),付费墙仍能正常工作。

3.4 Render trial and intro pricing from
pricingPhases

3.4 从
pricingPhases
渲染试用和入门价格

The first
PricingPhase
is the trial or intro price when present. Use
offerPaymentMode
for trial detection.
kotlin
val option = pkg.product.defaultOption ?: return
val first = option.pricingPhases.first()
val isTrial = first.offerPaymentMode == OfferPaymentMode.FREE_TRIAL
billingPeriod.value
is the count in the period's unit, not days. A
P1W
period gives
value = 1
,
unit = WEEK
. Build labels off both fields:
kotlin
val p = first.billingPeriod
val label = when (p.unit) {
    Period.Unit.DAY -> "${p.value} day"
    Period.Unit.WEEK -> "${p.value} week"
    Period.Unit.MONTH -> "${p.value} month"
    Period.Unit.YEAR -> "${p.value} year"
    else -> p.iso8601
}
第一个
PricingPhase
即为试用或入门价格(如果存在)。使用
offerPaymentMode
检测是否为试用。
kotlin
val option = pkg.product.defaultOption ?: return
val first = option.pricingPhases.first()
val isTrial = first.offerPaymentMode == OfferPaymentMode.FREE_TRIAL
billingPeriod.value
是周期单位的数量,而非天数。
P1W
周期对应的
value = 1
unit = WEEK
。结合两个字段构建标签:
kotlin
val p = first.billingPeriod
val label = when (p.unit) {
    Period.Unit.DAY -> "${p.value} day"
    Period.Unit.WEEK -> "${p.value} week"
    Period.Unit.MONTH -> "${p.value} month"
    Period.Unit.YEAR -> "${p.value} year"
    else -> p.iso8601
}

3.5 Prepaid plans

3.5 预付费方案

Prepaid base plans use the same
SubscriptionOption
API. Their
pricingPhases
report
RecurrenceMode.NON_RECURRING
. To accept pending purchases for prepaid plans, enable the flag at configuration time.
kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .pendingTransactionsForPrepaidPlansEnabled(true)
    .build()
预付费基础方案使用相同的
SubscriptionOption
API。它们的
pricingPhases
会返回
RecurrenceMode.NON_RECURRING
。要接受预付费方案的待处理购买,请在配置时启用该标志。
kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .pendingTransactionsForPrepaidPlansEnabled(true)
    .build()

3.6 Check access after purchase

3.6 购买后检查权限

Prefer entitlements. They reflect server computed access state including grace period, account hold, and cancellation with remaining time.
kotlin
val info = result.customerInfo
val isPro = info.entitlements["pro"]?.isActive == true
If you need the raw product ID, use
customerInfo.activeSubscriptions
. It returns a
Set<String>
of
"subscriptionId:basePlanId"
entries.
优先使用权益(entitlements)。它们反映了服务器计算的访问状态,包括宽限期、账户暂停和剩余时间内的取消状态。
kotlin
val info = result.customerInfo
val isPro = info.entitlements["pro"]?.isActive == true
如果你需要原始产品ID,请使用
customerInfo.activeSubscriptions
。它返回一个
Set<String>
,包含
"subscriptionId:basePlanId"
格式的条目。

Decision summary

决策总结

QuestionAnswer
How do I fetch products?
Purchases.sharedInstance.awaitOfferings()
then
offerings.current
.
How do I present durations?
offering.monthly
/
annual
/
weekly
or iterate
availablePackages
.
How do I purchase?Pass the
Package
to
PurchaseParams
and let
defaultOption
apply.
How do I target a specific offer?Filter
product.subscriptionOptions
by tag or ID, pass the
SubscriptionOption
.
How do I detect a free trial?
pricingPhases.first().offerPaymentMode == OfferPaymentMode.FREE_TRIAL
.
How do I check access?
customerInfo.entitlements["<id>"]?.isActive
.
问题答案
如何获取产品?
Purchases.sharedInstance.awaitOfferings()
然后使用
offerings.current
如何展示时长?使用
offering.monthly
/
annual
/
weekly
或遍历
availablePackages
如何进行购买?
Package
传递给
PurchaseParams
,让
defaultOption
生效。
如何定位特定优惠?通过标签或ID过滤
product.subscriptionOptions
,传递
SubscriptionOption
如何检测免费试用?
pricingPhases.first().offerPaymentMode == OfferPaymentMode.FREE_TRIAL
如何检查访问权限?
customerInfo.entitlements["<id>"]?.isActive

References

参考资料