订阅
RevenueCat 付费墙与账户绑定的权益读模型。
模板提供了一整套内购能力:由 RevenueCat 驱动的付费墙、恢复购买与兑换码,以及最关键的差异化设计—— 账户绑定的权益(entitlement)。它跨平台跟随用户,而不是存活在 RevenueCat SDK 里。本页说明这些 组件如何协作,好让你把自己的功能挂到这套权益之上。
subscriptions 表才是权威来源,RevenueCat SDK 不是。 RevenueCat 只知道 Apple 上的购买。
桌面端 / Creem 购买、促销发放或人工授予都只会写入这张表。每一处门禁都读这张表;SDK 只是购买手段
加一层本地快速缓存。切勿把 CustomerInfo 当作门禁来使用(或书写)。
提供了什么
| 界面 | 标签 | 说明 |
|---|---|---|
付费墙(PaywallView) | Integrated | 从 RevenueCat offering 渲染月付 / 年付 / 终身包 |
管理订阅(ManageSubscriptionView) | Integrated | 状态摘要 + 跳转 Apple 的管理订阅页面 |
兑换码(RedeemCodeView) | Integrated | 打开 StoreKit 的兑换码弹窗 |
高级门禁(PremiumGate) | Integrated | 把任意 UI 包在权益之后;内置指南文档正是它的使用者 |
所有这些在没有密钥时都会优雅降级。缺少 REVENUECAT_IOS_KEY 时,PurchasesController 保持
isConfigured == false,付费墙渲染“暂不可用”卡片,不会崩溃。
关键文件
两个控制器,各司其职
这套设计把“商店告诉我们什么”与“这个账户拥有什么权益”分开。两者都是 @MainActor @Observable,
存放在 AppEnvironment 中。
Prop
Type
PurchasesController 订阅 auth 会话流,登录时调用 Purchases.logIn(userId.uuidString),因此
RevenueCat 的 app_user_id 始终等于 Supabase 用户 UUID。正是这条身份绑定,让 Apple 上完成的购买
落到与桌面端购买相同的那一行 subscriptions。
账户绑定的流转
Apple (App Store) Creem(桌面 / 网页)
│ │
▼ ▼
revenuecat-webhook creem-webhook ← service-role Edge Functions
│ │ (唯一的写入方)
└──────────────┬─────────────┘
▼
public.subscriptions (每用户一行,RLS 只读自己)
│
▼
EntitlementController.isEntitled ← 跨平台门禁
│
▼
Home 卡片 · Me 行 · PremiumGate · 付费墙的“已拥有”状态购买会先通过 CustomerInfo 立即本地确认(让 UI 感觉即时响应),随后 PaywallViewModel 启动
EntitlementController.pollAfterPurchase()——一个短重试循环(0.8s → 1.6s → 3.0s),用来消化
webhook 的写入延迟,直到权威行出现。
isEntitled 规则
Subscription.isEntitled 是唯一的布尔门禁,与桌面端模板逐字一致,因此两端对“已拥有”的定义完全相同:
| 状态 | 何时视为已授权 |
|---|---|
active / trialing | one_time → 永远有效;否则 period_end > now |
canceled | period_end > now(自动续订已关,但仍在有效期内) |
unpaid / expired | 从不 |
unknown(更新的 schema) | 从不(宽松解码 → 视为未授权) |
已登出、加载中、出错状态从不授权——门禁绝不会在不确定状态下解锁。
每用户单行的冲突规则
由于用户可能同时在 Apple 与 Creem 购买,每次 webhook upsert 都要确定性地收敛到同一 user_id 行:
- 相同提供方 → 总是应用(提供方对自己的线路是权威的,因此续订 / 取消 / 过期都能通过)。
- 不同提供方 → 仅当传入的
period_end更晚时才应用。Apple 购买绝不会缩短仍有效的 Creem 计划,反之亦然。
该规则在 revenuecat-webhook(本模板)与 creem-webhook(桌面端)中的编码完全一致。
为你自己的功能加门禁
把任意视图包进 PremiumGate——它读取 EntitlementController(而非 SDK),渲染加载指示器、你的内容,
或一张锁定升级卡片:
PremiumGate(featureName: "高级导出", onUpgradeTap: { openPaywall() }) {
AdvancedExportView()
}需要命令式判断时,直接读 env.entitlements.isEntitled。切勿拿 purchases.isPro /
CustomerInfo 当门禁——它只看得到 Apple 购买,会漏掉跨平台与人工授予。
商品与权益
付费墙渲染 RevenueCat offering 中包含的任意包——应用没有硬编码的商品清单。模板的参考目录是三档, 全部挂到同一个权益上:
soar_starter_swift_pro_monthly (自动续订,1 个月)
soar_starter_swift_pro_yearly (自动续订,1 年)
soar_starter_swift_lifetime (非消耗型,一次性)任意一个都会翻转 isEntitled——这是单一档位模型(一份许可,用布尔门禁),而非按功能的矩阵。包按
月付 → 年付 → 终身排序,付费墙默认预选年付。购买会打上一个 PlanTier(pro / lifetime)标签,它
由 RevenueCat 的 PackageType 推导得出,仅用于 purchase completed 分析事件——无需任何应用侧的
商品 ID 配置。
权益标识符来自 REVENUECAT_PRO_ENTITLEMENT_ID(默认 pro)。它必须与 RevenueCat 里的标识符
完全一致(不是显示名),否则购买不会解锁——参见配置。
没有 RevenueCat 密钥? PurchasesController.isConfigured 为 false:付费墙显示“暂不可用”
卡片,PremiumGate 保持锁定,每次购买 / 恢复调用都会返回友好的失败信息。应用其余部分照常运行——
订阅对启动模板而言完全是可选项。
验证
在设好 REVENUECAT_IOS_KEY 且 offering 已上线后(参见
RevenueCat 配置):
- 登录后打开付费墙——包能加载出来,且默认预选年付。
- 没有密钥时,付费墙显示“暂不可用”卡片且不崩溃。
- 沙盒购买后,Home 许可卡片 / Me 行会在几秒内翻转为已拥有(
pollAfterPurchase循环追上了 webhook 写入)。
购买需要真实的 StoreKit 环境——模拟器无法对真实商品完成 Apple 的购买弹窗。参见 购买测试。
