计费
受开关控制的 Creem 结账、权益门控与许可证激活。
计费是一个默认关闭的 受开关控制 功能。保持
VITE_BILLING_ENABLED=false 时,结账操作、权益读取、功能门控和完整的
计费界面都会保持关闭,因此此模板同样可用于不销售订阅的桌面应用。
只有在 Supabase、计费迁移、Creem 产品和 Edge Function 密钥均已配置后, 才应启用它。服务商侧的配置见 Creem 配置。
开关
| 开关 | 默认值 | 作用 |
|---|---|---|
VITE_BILLING_ENABLED | false | 结账、权益门控和 /billing 的总开关。 |
VITE_BILLING_LICENSE_MODE | false | 显示可选的许可证密钥激活卡片。 |
以下内容均假定 VITE_BILLING_ENABLED=true。
价格配置
shared/price-config.ts 是渲染器侧的唯一价格映射,定义了三个层级:
| 层级 | 产品形态 | 产品环境变量 |
|---|---|---|
| Free | 无 Creem 产品 | 无 |
| Pro | 月付和年付订阅 | VITE_CREEM_PRODUCT_PRO_MONTHLY、VITE_CREEM_PRODUCT_PRO_YEARLY |
| Lifetime | 一次性购买 | VITE_CREEM_PRODUCT_LIFETIME |
产品 ID 是公开的构建配置,不是凭据;Creem API 密钥只能存放在 Supabase
Edge secrets 中。planTierForProductId() 将已知产品 ID 映射为 pro 或
lifetime;缺失或未知 ID 一律映射为 free,所以未配置或过期的产品不会
错误解锁付费功能。其结构与 soar-next 的价格配置一致,方便同时维护 Web
和桌面产品目录的团队。
结账与权益流程
购买在系统浏览器中完成:
- 已登录用户在
/billing选择付费方案。 startCheckout(productId)调用经过 JWT 验证的create-checkoutEdge Function。- 该函数创建 Creem Checkout,并新增一条
status = 'paying'、带生成order_no的payments台账记录。 - 渲染器只接受
https://*.creem.io的结账 URL,再通过 Tauri 外部打开器 启动系统浏览器。Capabilities ACL 将该打开器限定为https://**,而不是 授予任意原生能力。 - Creem 调用公开的
creem-webhook。它用 service-role 更新付款记录并 upsert 用户的subscriptions权益读取模型。 - 浏览器可通过
soar-tauri://billing?status=success&orderId=…返回,通常 会经过一个可选的托管跳转页。Billing 路由在打开时刷新;应用窗口重新获得 焦点时,store 也会刷新。
返回 URL 只是便利路径,不是付款凭据。应用只根据已验证 webhook 写入、受
RLS 保护的 subscriptions 行授予访问权限。
BILLING_SUCCESS_URL 是可选的 Edge secret。设置后,create-checkout
会附加 status=success&orderId=<orderNo>,并把 Creem 导向该托管页。模板
附带的页面会转发到 soar-tauri://billing;重新品牌化后请同步修改协议名。
权益与功能门控
src/stores/subscription-store.ts 计算由
src/lib/feature-gate.ts 使用的账号权益状态:
| 状态 | 含义 |
|---|---|
disabled | 计费已关闭;所有功能均允许,且不读取 Supabase。 |
unknown | Auth、网络或 RLS 查询失败;显示中性的等待状态。 |
free | 已登录用户没有订阅记录。 |
active | 权益处于 active、trialing,或已取消但仍在已付费周期内。一次性购买不会到期。 |
expired | 有记录,但已不再授予权限。 |
随模板提供的功能映射是可替换示例:
| 功能键 | 解锁层级 |
|---|---|
export_pdf | Pro、Lifetime |
sync_cloud | Pro、Lifetime |
unlimited_projects | Pro、Lifetime |
priority_support | Pro、Lifetime |
custom_branding | Lifetime |
使用 <FeatureGate> 包裹付费 UI。它接收 feature、children,以及可选的
fallback 和 pending:
<FeatureGate
feature="custom_branding"
pending={<MutedNotice />}
fallback={<UpgradePrompt />}
>
<CustomBrandingPanel />
</FeatureGate>unknown 会显示 pending,而不是付费墙。这样在临时离线或 RLS 查询失败时,
已付费用户不会短暂看到升级提示。
客户门户
管理订阅操作会调用经过 JWT 验证的 customer-portal Function。它从 Supabase
token 中确定用户,只读取该用户受 RLS 限制的订阅行内 customer_id,请求
Creem portal 会话,并仅返回已验证的 https://*.creem.io URL。渲染器会在
系统浏览器打开前再次验证该 URL。
可选许可证模式
许可证模式是面向 Creem license key 产品的替代激活路径,例如人工发放或一次性 许可证:
VITE_BILLING_LICENSE_MODE=trueLicenseKeyCard 将密钥发送至经过 JWT 验证的 activate-license Function。
该函数向 Creem 验证密钥,并将同一用户的 subscriptions 行 upsert 为 active 的
one_time 权益,且写入一个很远的到期时间。之后应用只通过 OS keychain 的白名单
license-key secure slot(Rust 中的 SecureKey::LicenseKey)保存该密钥。
本地密钥本身不授予权益;Supabase 订阅行仍是门控的唯一事实来源。停用时会将该行
标为 canceled,并且即使尽力执行的服务端释放请求不可达,也会清除本地密钥。当前模板中,
取消的订阅在 period_end 之前仍拥有权益;许可证激活将该日期写为 2999,因此停用
不会立即撤销权益。如需立即撤销,请修改 Function 以结束周期(或调整权益策略),
并测试该行为。结账和许可证激活共用每个用户唯一的订阅行,因此应将许可证模式作为
明确设计的替代购买路径,而非未经产品设计就同时打开。
验证计费
- 启用计费,并填入真实的测试环境
VITE_CREEM_PRODUCT_*ID。 - 部署 Edge Functions 并配置 Creem secrets。
- 登录,打开
/billing,开始 sandbox 结账。 - 确认系统浏览器打开
creem.io结账 URL。 - 确认
payments.status变为success,且对应的subscriptions行变为active。 - 通过
soar-tauri://billing返回或重新聚焦应用,确认当前方案会刷新。 - 若启用许可证模式,激活一个测试密钥,并在依赖本地 keychain 副本前确认订阅行已写入。
