跳到正文
Indiana
控制台
章节目录 / Funnel 接入与恢复

Funnel 接入与恢复

显式启动固定版本流程,处理身份、重试和结果。

SDK 示例对应当前 API。核对匹配版本与 HTTP 接入方式

API v1更新于

开始前准备

先接通独立身份与所需业务能力,再发布 Funnel 和固定引用。Funnel 用于编排;跨天等待与后台执行需要相应运行时,不能从图节点推定支持。

设计并显式启动

Funnel composes independent capabilities into a versioned flow. It owns graph execution and accepted results; Offering, Paywall, Coupon, commerce and GrowthBook retain their own definitions and business facts.

  • 在 Funnels 创建流程,连接 Page/Popup、条件、实验与已支持的能力节点。引用已发布 Paywall 时固定其 Version;发布保存当前图和资源引用。
  • 从你自己的 SDK/API 调用显式启动。匹配版本的后端可用 campaign_decide 固定活动决策、campaign_start 按 shown 记录实施频控;普通 decide/start 不自动启用。先验证宿主登录,再签发当前 Customer 会话。访客先创建 Guest 并取得 Guest 会话。
  • 记录原 requestId 和整个 start 输入,响应丢失时原样重试。SDK 安装要求见本页版本提示。
server/start-funnel.tsTypeScript
import {  createFunnelStartClient,  createIndianaServer,  createGuestToken,} from "@tinkerlandai/indiana-sdk/server"; const options = {  baseUrl: process.env.INDIANA_BASE_URL!,  apiKey: process.env.INDIANA_SECRET_KEY!,  scope: { projectId: process.env.INDIANA_PROJECT_ID!, environment: "sandbox" },} as const;const indiana = createIndianaServer(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. A signed-in user is named by your own user ID: Indiana creates the  // Customer on first use with creation-only customerData (needs customers:write). A visitor is a  // Guest kept in this session, referenced by its Indiana Customer ID.  let customer;  if (session.userId) {    customer = { type: "externalUserId", value: session.userId } as const;  } else {    session.indianaGuestToken ??= createGuestToken();    await session.save();    const guest = await indiana.createGuestCustomer({ guestToken: session.indianaGuestToken });    customer = { type: "id", value: guest.customerId } as const;  }  // Persist before the first request. Retries reuse the whole original input.  if (!session.funnelStart) {    session.funnelStart = {      funnelId: process.env.INDIANA_FUNNEL_ID!,      requestId: crypto.randomUUID(),      customer,      ...(session.userId ? { customerData: { attributes: { plan: session.plan } } } : {}),      locale: session.locale,      parameters: { commerce: { provider: "stripe", currency: "USD" } },    };    await session.save();  }  const result = await funnels.start(session.funnelStart);  // The browser runs the Funnel as that Customer.  const { customerToken } = session.userId    ? await indiana.createCustomerSession(result.customerId!)    : await indiana.resumeGuestSession({ guestToken: session.indianaGuestToken! });  session.indianaRunToken = result.runToken;  await session.save();  return Response.json({ runToken: result.runToken, customerToken }, {    headers: { "Cache-Control": "private, no-store" },  });}

展示、重试与身份切换

requireHostSession、session.save、reportError 和购买回调是需要你的应用实现的接入点。商业动作仍调用独立服务;当前配置下不支持的动作须显示明确错误,不能在浏览器标成已完成。

使用已领取的 Coupon 时,向 showFunnelRun 或 showPaywall 传入 resolveOfferId({ actionId, productVersionId }),返回所选 Coupon Grant 的 UUID;常规购买返回 undefined。SDK 在报价前选择一次,并在同一请求重试时保留该优惠。领券不会自动应用折扣,服务端仍校验归属、资格、有效期和 Product。自定义界面可向 session.preview 显式传入 offerId。

  • destroy 只清理界面;close 提交关闭命令。路由离开或换号后清理界面、请求与内存凭据,迟到结果不得覆盖新身份。
  • Guest 认领保留相同 UUID 时可以 link 继续;claimGuestCustomer 返回 switched=true 时,关闭原 Guest 界面,为返回 Customer 创建会话并启动新流程。历史和持有不会自动合并。
  • 每个 Funnel 独立配置 Local/Remote,Owner/Admin 通过单独设置更改。已接收的结果保持固定,新的请求使用当前规则;已下载 Local 规则无法收回。
  • Funnel 完成、按钮点击、回跳与自定义回执都不是付款或权益证明。用商业操作状态与 readHoldings 核对结果。

相关:Offering·Paywall·Version compatibility

client/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/indiana-funnel", {  method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken, customerToken } = await response.json();const client = createFunnelRunController({  baseUrl: "https://your-indiana-host.example",  scope: { projectId: "your-app-uuid", environment: "sandbox" },  runToken,});client.identify(customerToken);const handle = await showFunnelRun(container, {  client,  // Record-only purchase: your backend creates the Stripe Checkout Session from execution.  onPurchase: ({ operation }) => startMerchantCheckout(operation.execution, operation.operationId),  // External pages: without onOpenUrl the SDK opens this window or a new one as authored (openIn).  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.client.submit({ operation: "close", requestId: crypto.randomUUID(), nodeId: flow.client.view!.nodeId, visit: flow.client.view!.visit, revision: flow.client.view!.revision });

完成后检查

预期结果

保留固定版本、运行凭据和原命令,能区分等待、关闭、失败与完成;恢复不重复执行已接受的商业操作。