订阅

通过 RevenueCat 和 Supabase 读模型实现账号绑定的 Pro 权益。

Kotlin 模板通过 Google Play 和 RevenueCat 销售 Pro,但应用本身不会把 RevenueCat 当成权益真相。购买事件会写入共享的 Supabase subscriptions 表,应用门禁读取这条账号级记录。

提供什么

SurfaceStatusImplementation
RevenueCat SDK 生命周期IntegratedREVENUECAT_ANDROID_KEY 存在时由 PurchasesController 配置 RevenueCat
账号身份IntegratedRevenueCat 使用 Supabase user UUID 作为 app_user_id 登录
权益状态IntegratedEntitlementController 通过 SubscriptionRepository 读取 public.subscriptions
付费墙IntegratedPaywallScreen 渲染当前 offering 返回的 packages
管理订阅IntegratedManageSubscriptionScreen 展示 RevenueCat / Google Play 管理状态
兑换码IntegratedRedeemCodeScreen 打开 Google Play 兑换页面
Pro 内容示例Removable demofeature/guide/PremiumGate 锁住内置开发者指南

关键文件

权益流程

Google Play purchase
  -> RevenueCat customer event
  -> revenuecat-webhook Edge Function
  -> public.subscriptions, provider = play_store
  -> EntitlementController.state
  -> Home license card, Me row, PremiumGate, guide locks

关键点是账号绑定权益。PurchasesController 观察 SessionController.currentUser,调用 RevenueCat logIn(userId),并把 Supabase Auth UUID 作为 RevenueCat app_user_id。Webhook 会拒绝非 UUID 的 app_user_id,所以匿名 RevenueCat customer 不能解锁账号门禁内容。

EntitlementController 是应用侧权威。它读取当前用户的 subscriptions 行, 暴露 EntitlementState,登录后刷新,退出后清空,RevenueCat customer-info 更新后刷新,并在购买后轮询一小段时间等待 webhook 写入。

RevenueCat 仍然很重要:它提供 Google Play Billing UI、当前 offering、价格、 restore 流程、customer-info 更新和管理 URL。它不是决定 Pro 内容是否解锁的门禁。

商品模型

模板预期一个 RevenueCat entitlement:

const val PRO_ENTITLEMENT_ID = "pro"

这个常量在 PurchasesController.Companion 中。如果你重命名 entitlement,要同步修改 这里和 RevenueCat 控制台。

推荐的 Google Play product id:

Product idStore typeSupabase product_typeAccess model
pro_monthly自动续订订阅,月付 base planmonthactive、trialing 或已取消但仍在已付费周期内时有权益
pro_yearly自动续订订阅,年付 base planyear同样规则,周期为一年
pro_lifetime一次性应用内商品one_time激活后永久有权益

这些 product id 没有硬编码在 Android 应用里。RevenueCat offering 决定付费墙收到的 子集、价格、本地化文案和 package identifier。

付费墙行为

PaywallViewModel 读取当前 offering,并按下面顺序排序 packages:

  1. Monthly
  2. Annual
  3. Lifetime
  4. 其他 RevenueCat package type

如果 Annual 存在,会默认选中 Annual。否则选中排序后的第一个 package。UI 会渲染 offering 返回的任意子集,所以构建可以只提供月付/年付、只提供 Lifetime,或三者都提供。

PaywallScreen 的状态行为:

State用户看到的行为
RevenueCat 尚未 readyLoading spinner
Supabase 权益已有效已拥有 license card
缺少 REVENUECAT_ANDROID_KEY提示当前构建无法使用应用内购买
Offering 没有 packages可重试的加载错误块
Packages 存在选择 package、继续购买、restore、兑换码、法律链接

Annual package 会显示 "Most popular",并在月付和年付价格可计算出有效折扣时显示 savings。 Lifetime package 会显示 "Best value"。

被锁定的指南

应用内 Pro 示例是开发者指南:

  • GuideIndexScreen 在用户未拥有权益时展示指南列表和锁图标。
  • GuideDocumentScreenPremiumGate 包住文档正文。
  • PremiumGate 只有在 EntitlementState.isEntitled() 为 true 时显示内容。

这些指南属于可移除的 Showcase 分支。你可以把它们当作自己 Pro-only 功能的模式参考; 删除 Showcase tab 时也可以一起删除或替换。

权益规则

Subscription.isEntitled(now) 的规则刻意保持简单:

Subscription statusProduct typeEntitled when
active / trialingone_time永远
active / trialingmonth / yearperiod_end 在未来
canceledmonth / year仍处于已付费周期内
unpaid / expired / unknown任意从不

未知 enum 值会解码为 Unknown,默认锁住,直到应用显式支持这个新值。

跨平台行归属

public.subscriptions 每个用户只有一行。RevenueCat webhook 对同 provider 的 Play Store 事件总是应用,因为 Play 对自己的续订、取消、账单问题和过期状态是权威。

如果当前行来自另一个 provider,只有 incoming event 延长 period_end 时才会接管。 这意味着较短的 Play Store 订阅不会缩短更长的 Creem 或 App Store 权益,其他平台的 较短事件也不会缩短当前有效的 Play Store 周期。

验证

  • 添加 REVENUECAT_ANDROID_KEY,登录后确认 RevenueCat 使用 Supabase user UUID 登录。
  • 打开付费墙,确认当前 offering 的 packages 能渲染。
  • 移除 key、重新构建,确认付费墙显示 unavailable 信息而不是崩溃。
  • 完成 sandbox purchase,确认解锁来自 subscriptions 行,而不只是 RevenueCat CustomerInfo
  • 在 Play 测试环境取消订阅,确认访问会保留到 period_end

相关页面

On this page