跳到正文
Indiana
控制台
文档
章节目录 / 引导页与 Funnel

引导页与 Funnel

连接独立页面、条件分支、认证和购买节点。

SDK 0.0.1更新于

设计流程

在控制台设计每个页面,再通过流程画布连接按钮、条件、认证、购买与完成节点。返回沿实际访问路径运行;默认选项不等于用户作答。发布版本固定页面文档与流程结构。

预览会模拟认证与购买。真实 Funnel 的购买成功路径只有在付款和权益均确认后才进入。

启动与展示

在自己的后端使用 createFunnelStartClient().start() 启动运行,必须带客户会话:已登录用户用客户会话;未登录访客先 createGuestCustomer,再用 resumeGuestSession 取得游客会话。24 小时 runToken 用于浏览器运行。游客登录认领后,运行通过 link 继续,分组不变。

app/onboarding.tsTypeScript
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().

恢复与实验

  • 运行固定发布版本与分桶配置;登录后保留原分组。
  • 服务端保存当前节点,浏览器答案由宿主按同版本保存;destroy() 只清 UI,close() 另外提交关闭事件。
  • 认证回调获取自己的后端为当前登录用户签发的客户 Token,不传 customerId 证明登录。
  • 实验报告按真实曝光比较;客户端完成事件不证明收入。

在宿主后端启动

此页描述当前源码协议,SDK、Indiana 部署与数据库迁移须匹配;npm 已发布 0.0.1 beta,不代表后续工作区版本已发布。宿主负责真实会话与持久化 start 输入,切换账号须关闭并丢弃旧运行。

相关:服务端回执、任务与恢复

app/api/groove-funnel/route.tsTypeScript
import {  createFunnelStartClient,  createGrooveServer,  createGuestToken,} from "@tinkerlandai/indiana-sdk/server"; const options = {  baseUrl: process.env.GROOVE_BASE_URL!,  apiKey: process.env.GROOVE_SECRET_KEY!,  scope: { projectId: process.env.GROOVE_PROJECT_ID!, environment: "sandbox" },} as const;const groove = createGrooveServer(options);const funnels = createFunnelStartClient(options); export async function POST(request: Request) {  if (request.headers.get("origin") !== process.env.APP_ORIGIN)    return new Response("Forbidden", { status: 403 });  const session = await requireHostSession(request);  // Every run has a customer: the signed-in customer, or a guest kept in this session.  let customerToken: string;  if (session.grooveCustomerId) {    ({ customerToken } = await groove.createCustomerSession(session.grooveCustomerId));  } else {    session.grooveGuestToken ??= createGuestToken();    await session.save();    await groove.createGuestCustomer({ guestToken: session.grooveGuestToken });    ({ customerToken } = await groove.resumeGuestSession({ guestToken: session.grooveGuestToken }));  }  // Persist before the first request. Retries reuse the whole original input.  if (!session.funnelStart) {    session.funnelStart = {      source: { type: "funnel", id: process.env.GROOVE_FUNNEL_ID! },      requestId: crypto.randomUUID(),      locale: session.locale,      commerce: { provider: "stripe", currency: "USD" },    };    await session.save();  }  const result = await funnels.start({ ...session.funnelStart, customerToken });  session.grooveRunToken = result.runToken;  await session.save();  return Response.json({ runToken: result.runToken }, {    headers: { "Cache-Control": "private, no-store" },  });}

展示、恢复与客户组件

obtainCurrentUserCustomerToken 先完成宿主登录,再向自己的后端获取当前用户 Token;取消返回 null。requireHostSession、session.save 与 reportError 是宿主需要提供的接入点。保留原启动输入,付款返回后重新展示相同运行。

  • 账户、会员、积分和账单是 Funnel 屏幕组件。组件读取当前认证客户;购买和会员按钮必须在编辑器连接到明确的节点出口。
  • Paywall 可用 source: { type: 'paywall', id } 独立展示,服务端使用其系统维护的 Funnel。无需独立 Page 或远程 Popup 接口。
  • destroy 仅移除界面;close 提交关闭动作。浏览器缓存只保存设计资产,客户数据与运行状态保持私有。
app/groove-funnel.tsTypeScript
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().