# 复制你的 App → Sandbox → SDK 接入中的配置GROOVE_BASE_URL=https://your-grow-host.exampleGROOVE_PROJECT_ID=your-project-idGROOVE_SECRET_KEY=your-sandbox-server-key # 浏览器可见;不包含 Secret KeyNEXT_PUBLIC_GROOVE_BASE_URL=https://your-grow-host.exampleNEXT_PUBLIC_GROOVE_PROJECT_ID=your-project-id代码示例
完整接入文件与常见业务场景,按你的项目替换登录校验和配置。
下载接入示例SDK 0.0.1更新于
Next.js:身份与客户会话
按快速开始的顺序使用这些文件。唯一需要接入的宿主函数是你已有的 getSignedInUser;必须验证真实登录会话。
刷新会话与退出登录
在已有 grow 客户端旁增加这个模块。只在 UNAUTHENTICATED 时刷新一次;失败向上抛出。退出登录或切换账号调用 disconnectSession,中止宿主会话获取和 SDK 旧请求;再调用自己的登出接口。写入重试沿用原 requestId / 幂等键。
import { GrooveError } from "@tinkerlandai/indiana-sdk";import { grow } from "./groove-client"; // 向你自己的服务端取新 Token(会话接口需要你的登录态)let generation = 0;let pending: { controller: AbortController; promise: Promise<void> } | undefined; function refreshSession(): Promise<void> { if (pending) return pending.promise; const expected = generation; const controller = new AbortController(); const promise = (async () => { const response = await fetch("/api/groove-session", { signal: controller.signal, cache: "no-store", credentials: "same-origin", }); if (!response.ok) throw new Error("用户未登录"); const { customerToken } = await response.json(); if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed during refresh"); if (typeof customerToken !== "string") throw new Error("Invalid session"); grow.identify(customerToken); })(); pending = { controller, promise }; const clear = () => { if (pending?.promise === promise) pending = undefined; }; void promise.then(clear, clear); return promise;} // Token 过期(默认 10 分钟)时重新签发一次再重试export async function withSession<T>(call: () => Promise<T>) { const expected = generation; try { return await call(); } catch (error) { if (!(error instanceof GrooveError) || error.code !== "UNAUTHENTICATED") throw error; if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed"); await refreshSession(); if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed"); return call(); }} // 你的用户退出登录时:先丢弃 Token,再走你自己的登出export function disconnectSession() { generation++; pending?.controller.abort(); pending = undefined; grow.reset(); // 清除客户 Token,中止进行中的请求} export async function signOut() { disconnectSession(); await fetch("/api/logout", { method: "POST" });}从已发布套餐创建结账
这是浏览器展示片段;先按 Funnel 接入页实现 /api/groove-funnel 后端及宿主回调。发布当前商品并配置 Sandbox 支付绑定,再发布引用固定商品版本的 Funnel 结账节点。宿主后端保留原启动输入;返回支付页后恢复同一运行。超时重试原命令,不直接创建另一笔结账。
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://grow.your-domain.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});const handle = await showFunnelRun(container, { client, onAuthenticate: async () => obtainCurrentUserCustomerToken(), onCheckout: ({ url }) => window.location.assign(url), onOpenUrl: ({ url }) => window.location.assign(url), onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().服务端:执行任务前消费积分
消费函数接受后端已经验证并关联的 customerId。jobId 是你的稳定任务 ID,重试沿用同一个幂等 Key。在服务端环境变量 GROOVE_GENERATIONS_ENTITLEMENT_ID 填写generations 功能的稳定 ID(不是 key 或商品版本 ID)。只有 accepted 为 true 才执行受保护的任务;失败退款或长任务预占见积分指南。
import { groove } from "./groove-server"; const configuredEntitlementId = process.env.GROOVE_GENERATIONS_ENTITLEMENT_ID;if (!configuredEntitlementId) throw new Error("Missing GROOVE_GENERATIONS_ENTITLEMENT_ID"); export async function authorizeGeneration(customerId: string, jobId: string) { const { receipt } = await groove.mutateBalance({ customerId, command: { operation: "consume", entitlementId: configuredEntitlementId, quantity: "1", properties: {}, idempotencyKey: `job:${jobId}:consume`, }, }); if (!receipt.accepted) throw new Error(receipt.reason ?? "Credits unavailable"); return receipt;}浏览器:展示已发布的 Funnel
先发布 Funnel 及入口配置。宿主后端调用 start,浏览器通过运行凭据展示。reportError 处理失败;页面离开或身份切换前销毁并丢弃旧运行。
相关:查看编辑器与客户预览
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://grow.your-domain.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});const handle = await showFunnelRun(container, { client, onAuthenticate: async () => obtainCurrentUserCustomerToken(), onCheckout: ({ url }) => window.location.assign(url), onOpenUrl: ({ url }) => window.location.assign(url), onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().