订阅

RevenueCat 付费墙与账户绑定的权益读模型。

模板提供了一整套内购能力:由 RevenueCat 驱动的付费墙、恢复购买与兑换码,以及最关键的差异化设计—— 账户绑定的权益(entitlement)。它跨平台跟随用户,而不是存活在 RevenueCat SDK 里。本页说明这些 组件如何协作,好让你把自己的功能挂到这套权益之上。

subscriptions 表才是权威来源,RevenueCat SDK 不是。 RevenueCat 只知道 Apple 上的购买。 桌面端 / Creem 购买、促销发放或人工授予都只会写入这张表。每一处门禁都读这张表;SDK 只是购买手段 加一层本地快速缓存。切勿把 CustomerInfo 当作门禁来使用(或书写)。

提供了什么

界面标签说明
付费墙(PaywallViewIntegrated从 RevenueCat offering 渲染月付 / 年付 / 终身包
管理订阅(ManageSubscriptionViewIntegrated状态摘要 + 跳转 Apple 的管理订阅页面
兑换码(RedeemCodeViewIntegrated打开 StoreKit 的兑换码弹窗
高级门禁(PremiumGateIntegrated把任意 UI 包在权益之后;内置指南文档正是它的使用者

所有这些在没有密钥时都会优雅降级。缺少 REVENUECAT_IOS_KEY 时,PurchasesController 保持 isConfigured == false,付费墙渲染“暂不可用”卡片,不会崩溃。

关键文件

PurchasesController.swift
EntitlementController.swift
EntitlementSnapshot.swift
PremiumGate.swift

两个控制器,各司其职

这套设计把“商店告诉我们什么”与“这个账户拥有什么权益”分开。两者都是 @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 / trialingone_time → 永远有效;否则 period_end > now
canceledperiod_end > now(自动续订已关,但仍在有效期内)
unpaid / expired从不
unknown(更新的 schema)从不(宽松解码 → 视为未授权)

已登出、加载中、出错状态从不授权——门禁绝不会在不确定状态下解锁。

每用户单行的冲突规则

由于用户可能同时在 Apple 与 Creem 购买,每次 webhook upsert 都要确定性地收敛到同一 user_id 行:

  1. 相同提供方 → 总是应用(提供方对自己的线路是权威的,因此续订 / 取消 / 过期都能通过)。
  2. 不同提供方 → 仅当传入的 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——这是单一档位模型(一份许可,用布尔门禁),而非按功能的矩阵。包按 月付 → 年付 → 终身排序,付费墙默认预选年付。购买会打上一个 PlanTierpro / lifetime)标签,它 由 RevenueCat 的 PackageType 推导得出,仅用于 purchase completed 分析事件——无需任何应用侧的 商品 ID 配置

权益标识符来自 REVENUECAT_PRO_ENTITLEMENT_ID(默认 pro)。它必须与 RevenueCat 里的标识符 完全一致(不是显示名),否则购买不会解锁——参见配置

没有 RevenueCat 密钥? PurchasesController.isConfiguredfalse:付费墙显示“暂不可用” 卡片,PremiumGate 保持锁定,每次购买 / 恢复调用都会返回友好的失败信息。应用其余部分照常运行—— 订阅对启动模板而言完全是可选项。

验证

在设好 REVENUECAT_IOS_KEY 且 offering 已上线后(参见 RevenueCat 配置):

  • 登录后打开付费墙——包能加载出来,且默认预选年付。
  • 没有密钥时,付费墙显示“暂不可用”卡片且不崩溃。
  • 沙盒购买后,Home 许可卡片 / Me 行会在几秒内翻转为已拥有(pollAfterPurchase 循环追上了 webhook 写入)。

购买需要真实的 StoreKit 环境——模拟器无法对真实商品完成 Apple 的购买弹窗。参见 购买测试

相关页面

On this page