开发指南 功能指南 会员与订阅 直接购买、变更、取消与恢复订阅,也可通过 Funnel 执行。
SDK 0.0.1 更新于 2026年10月14日
预览、确认与恢复购买 使用 npm @tinkerlandai/indiana-sdk@0.0.1 与匹配的服务端。先保存当前功能定义并发布会员商品,在当前 App 的 Sandbox 配置 Stripe 测试连接及价格绑定。Key 需要 billing:write;读取持有另需 entitlements:read。下面是可信后端的业务片段,groove 来自快速开始的 lib/groove-server.ts;customerId 必须由自己的已验证登录会话关联。
previewAttach 保存 15 分钟报价,不发起付款。attach 返回 Checkout URL 不代表已付;complete 才表示操作完成,权益再用 readHoldings 核对。响应丢失保留 operationId,不重新生成订单。未接受的报价过期或选择变化时才用新 requestId 重新报价。浏览器 SDK 的同名方法不传 customerId。
选择优惠时,先 listOffers,再传所选持券的 id 到 previewAttach.offerId;不是 Coupon 定义 ID。首次 Stripe Checkout 最多一张券,修改选择需重新报价。金额为币种的整数最小单位,自动税以渠道确认结果为准。多个套餐须满足服务端共同连接、币种、收费周期等条件;原生购买使用固定 SKU 和商店验单。
server/subscription-purchase.ts · fragment TypeScript 复制 1 import { groove } from "@/lib/groove-server" ;2 3 // Persist this complete input before requesting a quote; reuse it on retry. 4 const input = {5 customerId: verifiedCustomer.id, 6 requestId: retainedRequestId, 7 plans: [{ productVersionId: selectedPublishedVersionId }], 8 provider: "stripe" as const , 9 currency: "USD" as const , 10 }; 11 const { quote } = await groove.previewAttach (input);12 // Save quote.operationId. Show the quote and obtain explicit customer acceptance. 13 // Execute only after that acceptance, using the same verified customer. 14 const { operation } = await groove.attach ({15 customerId: verifiedCustomer.id, 16 operationId: quote.operationId, 17 }); 18 // Return operation.checkoutUrl to the browser when present. 19 // After payment return or a lost response, recover this original operation: 20 const recovered = await groove.readSubscriptionOperation ({21 customerId: verifiedCustomer.id, 22 operationId: quote.operationId, 23 }); 套餐变更、席位与取消续费 Stripe 使用 immediate 或 renewal,明确操作目标会员并保留同订阅其他套餐。pending 表示等待付款或恢复,scheduled 表示已排期,effective 才表示变更生效;已验证付款决定收费权益交付。席位变更使用 change: { type: 'seats', licenseKey, quantity };预付 Feature 数量使用 { type: 'quantity', entitlementVersionId, quantity },减少数量在 renewal 生效。取消/恢复续费使用 cancel/uncancel,取消与退款是独立操作。
原生套餐替换使用 timing: store,由宿主处理商店指令和验证交易;不能把 Stripe 差额报价或可变席位套到 Apple/Google。发布包含接口不代表你的部署、SKU、试用资格或实际支付已验证。
server/subscription-update.ts · fragment TypeScript 复制 1 const { quote } = await groove.previewUpdate ({2 customerId: verifiedCustomer.id, 3 requestId: retainedChangeRequestId, 4 membershipId: currentMembershipId, 5 timing: "immediate" , 6 change: { type : "plan" , productVersionId: targetPublishedVersionId }, 7 }); 8 // Persist the quote, show its amounts and timing, and obtain customer acceptance. 9 await groove.update ({10 customerId: verifiedCustomer.id, 11 operationId: quote.operationId, 12 }); 13 const { operation } = await groove.readSubscriptionUpdate ({14 customerId: verifiedCustomer.id, 15 operationId: quote.operationId, 16 }); Funnel 结账与会员管理 在商品中发布价格、权益和试用,再绑定当前环境的支付渠道。在 Funnel 中配置结账或会员动作,并通过明确的成功、取消和失败出口继续。
待付款保持等待;回跳不证明付款。购买、退款和会员变更以服务端验证事实与实际交付为准。只使用明确配置的 Sandbox 账户。
app/billing.ts TypeScript 复制 1 import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk" ;2 3 // Your backend retains the original start request and run credential. 4 export async function openFunnel (container: HTMLElement ) {5 const response = await fetch ("/api/groove-funnel" , {6 method: "POST" , credentials: "same-origin" , cache: "no-store" , 7 }); 8 if (!response.ok) throw new Error ("Funnel bootstrap failed" );9 const { runToken } = await response.json ();10 const client = createFunnelRunController ({11 baseUrl: "https://grow.your-domain.example" , 12 scope: { projectId: "your-app-uuid" , environment: "sandbox" }, 13 runToken, 14 }); 15 const handle = await showFunnelRun (container, {16 client, 17 onAuthenticate: async () => obtainCurrentUserCustomerToken (), 18 onCheckout: ({ url }) => window.location.assign (url), 19 onOpenUrl: ({ url }) => window.location.assign (url), 20 onError: reportError, 21 }); 22 // Keep the host's original bootstrap across navigation and payment returns. 23 // Destroy removes UI; close is an explicit server command. 24 return { handle, client, destroy () { handle.destroy (); client.reset (); } };25 } 26 // On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().