@appfunnel-dev/sdk 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +69 -3
  2. package/dist/{checkout-BYcOfyg_.d.ts → checkout-BPYU4wXF.d.ts} +50 -16
  3. package/dist/{checkout-BM1T-u6_.d.cts → checkout-BzP__Mxe.d.cts} +50 -16
  4. package/dist/{chunk-PN7CRJAF.js → chunk-AW2YURFE.js} +60 -19
  5. package/dist/chunk-AW2YURFE.js.map +1 -0
  6. package/dist/{chunk-MBTHQP4Z.cjs → chunk-DTSLJ24T.cjs} +11 -3
  7. package/dist/chunk-DTSLJ24T.cjs.map +1 -0
  8. package/dist/{chunk-NA7DRFOA.cjs → chunk-MBSXEIXT.cjs} +60 -19
  9. package/dist/chunk-MBSXEIXT.cjs.map +1 -0
  10. package/dist/{chunk-KVMNVX6J.js → chunk-USAECFWO.js} +11 -3
  11. package/dist/chunk-USAECFWO.js.map +1 -0
  12. package/dist/driver-paddle.cjs +14 -14
  13. package/dist/driver-paddle.d.cts +1 -1
  14. package/dist/driver-paddle.d.ts +1 -1
  15. package/dist/driver-paddle.js +1 -1
  16. package/dist/driver-stripe.cjs +31 -15
  17. package/dist/driver-stripe.cjs.map +1 -1
  18. package/dist/driver-stripe.d.cts +1 -1
  19. package/dist/driver-stripe.d.ts +1 -1
  20. package/dist/driver-stripe.js +18 -2
  21. package/dist/driver-stripe.js.map +1 -1
  22. package/dist/index.cjs +78 -67
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +25 -6
  25. package/dist/index.d.ts +25 -6
  26. package/dist/index.js +13 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/{manifest-BajvtPWs.d.cts → manifest-Dp1oRvqp.d.cts} +23 -0
  29. package/dist/{manifest-BajvtPWs.d.ts → manifest-Dp1oRvqp.d.ts} +23 -0
  30. package/dist/manifest-entry.cjs +9 -1
  31. package/dist/manifest-entry.cjs.map +1 -1
  32. package/dist/manifest-entry.d.cts +15 -2
  33. package/dist/manifest-entry.d.ts +15 -2
  34. package/dist/manifest-entry.js +9 -1
  35. package/dist/manifest-entry.js.map +1 -1
  36. package/package.json +1 -1
  37. package/dist/chunk-KVMNVX6J.js.map +0 -1
  38. package/dist/chunk-MBTHQP4Z.cjs.map +0 -1
  39. package/dist/chunk-NA7DRFOA.cjs.map +0 -1
  40. package/dist/chunk-PN7CRJAF.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/state/measurementSystem.ts","../src/flow/url.ts","../src/tracking/snapshotPersistence.ts","../src/tracking/platformTracker.ts","../src/flow/mount.tsx","../src/flow/components.tsx","../src/download/store.ts","../src/download/DownloadButton.tsx","../src/tracking/catalog.ts","../src/embed/script.tsx"],"names":["useLocale","useState","useEffect","createConsoleTracker","exposurePayload","createContext","createElement","useContext","Fragment","CheckoutResume","EmbedBridge","FunnelProvider","FunnelView","useField","useNavigation","useFunnel","useRef","useCallback"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `useMeasurementSystem()` — which unit system the current visitor most likely\n * *expects* (imperial ft/lbs vs metric cm/kg). A funnel seeds its own unit toggle\n * from it, e.g. BetterMe's height/weight steps default to ft/lbs for a US visitor\n * and cm/kg elsewhere.\n *\n * ## What it reports (and what it does NOT)\n * This hook reports an **expectation**, derived from where the visitor is / what\n * their browser advertises. It does **not** store the user's choice — that stays\n * funnel-owned (e.g. `data.units`). Intended use is one-shot seeding of local state:\n *\n * ```ts\n * const initialUnits = useMeasurementSystem() // 'imperial' | 'metric'\n * const [units, setUnits] = useState(initialUnits)\n * ```\n *\n * ## Detection sources, in priority order\n * 1. **Snapshot context region** — `useLocale().region` (the `context.locale`\n * region subtag). When the renderer injects a server-consistent locale (see\n * `buildContext` overrides), this is the country/region code; otherwise it is\n * the region subtag `detect()` reads from `navigator.language` on the client.\n * 2. **`navigator.language` region subtag** — direct fallback (e.g. `en-US` → `US`)\n * for the case where the context region is empty but the browser advertises one.\n * 3. **`'metric'`** — the final fallback (the world default, and the SSR default).\n *\n * ## The rule\n * Imperial is returned for the only countries that are *fully* imperial — the\n * United States (`US`), Liberia (`LR`), and Myanmar (`MM`) — **plus the United\n * Kingdom (`GB`)**. Everything else (and anything unknown) is metric.\n *\n * ### The UK decision (deliberate)\n * The UK is officially metric but, for **body measurements specifically**, everyday\n * usage is imperial-leaning: people give height in feet/inches and bodyweight in\n * stones/pounds. Since this hook exists to seed *body-measurement* toggles (height/\n * weight), `GB` returns `'imperial'` — the default a UK visitor is most likely to\n * expect on a weight/height step. The return type stays binary on purpose; a funnel\n * that wants UK-specific **stones** handling (which imperial-vs-metric can't express)\n * should branch on the country itself (`useLocale().region === 'GB'`) rather than on\n * this hook.\n *\n * ## SSR / hydration safety\n * Prerendered shells are rendered on the server with **no visitor context** (region\n * is empty server-side, real only on the client), so reading the region during\n * render would diverge between the server HTML and the first client paint and cause\n * a hydration mismatch. Following the codebase contract (\"seed from injected data,\n * never client-side detection on first paint\"), this hook returns `'metric'` for the\n * first render on **both** sides — a byte-clean hydration — then resolves the real\n * system in a **post-mount effect** and updates. Funnels seeding local state from it\n * therefore start metric and flip to imperial for imperial-region visitors on the\n * first commit (imperceptible, and correct once settled).\n */\n\nimport { useEffect, useState } from 'react'\nimport { useLocale } from './namespaces'\n\nexport type MeasurementSystem = 'imperial' | 'metric'\n\n/**\n * ISO-3166-1 alpha-2 codes that seed an imperial default. `US`/`LR`/`MM` are the\n * only fully-imperial countries; `GB` is added deliberately for body measurements\n * (see the module JSDoc — UK bodyweight/height are conventionally imperial).\n */\nconst IMPERIAL_COUNTRIES = new Set(['US', 'LR', 'MM', 'GB'])\n\n/**\n * Pure rule: map a country/region code to a measurement system. Returns `null` when\n * the input is absent or not a 2-letter code (so the caller can fall through to the\n * next source); a *known* metric country returns `'metric'`, not `null`.\n */\nexport function measurementSystemForCountry(\n\tcountry: string | null | undefined,\n): MeasurementSystem | null {\n\tif (!country) return null\n\tconst cc = country.trim().toUpperCase()\n\tif (!/^[A-Z]{2}$/.test(cc)) return null\n\treturn IMPERIAL_COUNTRIES.has(cc) ? 'imperial' : 'metric'\n}\n\n/** The region subtag of `navigator.language` (e.g. `en-US` → `US`), or undefined. Client-only. */\nfunction navigatorRegion(): string | undefined {\n\tif (typeof navigator === 'undefined') return undefined\n\tconst tag = navigator.language\n\tif (!tag) return undefined\n\tconst parts = tag.split('-')\n\tif (parts.length < 2) return undefined // language-only tag, no region\n\tconst region = parts[parts.length - 1]\n\treturn /^[A-Za-z]{2}$/.test(region) ? region : undefined\n}\n\n/**\n * Pure resolver over the priority chain: context region → `navigator.language`\n * region → `'metric'`. Reads `navigator` (client-only) — call it in an effect, not\n * during render (see the hook). Exported for direct/unit use.\n */\nexport function resolveMeasurementSystem(\n\tcontextRegion?: string | null,\n): MeasurementSystem {\n\treturn (\n\t\tmeasurementSystemForCountry(contextRegion) ??\n\t\tmeasurementSystemForCountry(navigatorRegion()) ??\n\t\t'metric'\n\t)\n}\n\n/**\n * Seed a funnel's unit toggle from the visitor's likely-expected measurement system.\n * SSR-safe: `'metric'` on first paint (server + hydration), resolved post-mount. See\n * the module JSDoc for the rule, the UK decision, and the detection priority.\n */\nexport function useMeasurementSystem(): MeasurementSystem {\n\t// Read during render (safe — not used for first-paint output) to feed the effect\n\t// and re-resolve if the injected region ever changes.\n\tconst region = useLocale().region\n\tconst [system, setSystem] = useState<MeasurementSystem>('metric')\n\tuseEffect(() => {\n\t\tsetSystem(resolveMeasurementSystem(region))\n\t}, [region])\n\treturn system\n}\n","/**\n * URL PATH SYNC — the funnel's live page in the address bar.\n *\n * Funnel navigation is a React state swap, not a document navigation, so nothing moves the URL on\n * its own: without this the visitor walks 30 screens while the address bar still reads the campaign\n * entry, and a refresh (or a share, or a browser restore) drops them back at page one.\n *\n * The renderer injects the campaign's path (`MountOpts.basePath`, e.g. `/get-started` or\n * `/de/get-started`) and the SDK writes `<basePath>/<page slug>` on every page change. The\n * START page keeps the bare campaign URL — that is the canonical ad destination, and a reload there\n * means \"start over\", which is exactly what the bare URL does.\n *\n * `replaceState`, never `pushState` (v0 behavior, deliberate): funnel back is in-memory and\n * branch-aware, so browser Back must stay an EXIT from the funnel rather than an untracked jump\n * backwards through it. Query (utm/click ids) and hash survive every rewrite.\n */\n\n/** Rewrite the address bar to the live page. No-op on the server, without a basePath, or if unchanged. */\nexport function syncFunnelUrl(basePath: string, slug: string, isStart: boolean): void {\n\tif (typeof window === 'undefined' || !basePath) return\n\tconst base = basePath.replace(/\\/+$/, '')\n\t// Encode per SEGMENT: a slug may legitimately contain slashes (funnels that mirror a source\n\t// site's nested paths), and those must stay real path separators — percent-encoding them would\n\t// produce a URL the renderer can't map back to the page.\n\tconst path = isStart\n\t\t? base || '/'\n\t\t: `${base}/${slug.split('/').map(encodeURIComponent).join('/')}`\n\tconst { search, hash, pathname } = window.location\n\tif (path === pathname) return\n\ttry {\n\t\twindow.history.replaceState(window.history.state, '', `${path}${search}${hash}`)\n\t} catch {\n\t\t// Sandboxed/cross-origin-restricted frames can throw on history writes — the funnel still runs.\n\t}\n}\n","/**\n * Durable snapshot persistence engine (audit fix: fire-and-forget → durable).\n *\n * The v2 namespace store's \"biggest magic\" is server-backed, cross-device answer\n * durability: every `responses.set(...)` / `user.set(...)` pushes the FULL flat\n * snapshot, which the tracker debounces (800ms, leading governor in\n * platformTracker) and POSTs to the v1 session-data endpoint. Before this engine\n * the POST was fire-and-forget: no retry, no queue, no flush-on-exit, no offline\n * tolerance — a single failed POST silently lost the visitor's answers, breaking\n * the \"your progress survives a reload\" promise.\n *\n * This engine sits BEHIND the 800ms debounce (the debounce stays the write-rate\n * governor; the engine is the durability layer). It owns one endpoint: sending\n * the latest full-state snapshot, coalesced, with retry/backoff, and a\n * synchronous flush on page exit.\n *\n * ── LATEST-SNAPSHOT-WINS ─────────────────────────────────────────────────────\n * Snapshots are full-state (the entire flat store), so ONLY THE LATEST MATTERS.\n * We never queue a backlog — at most one request is in flight and at most one\n * snapshot is pending. A newer snapshot arriving while one is in flight simply\n * REPLACES the pending payload; it is never appended.\n *\n * ── OUT-OF-ORDER / VERSION GUARD ─────────────────────────────────────────────\n * The server (apps/api …/session/data → FunnelTrackingController.updateSessionData)\n * merges each POST per-key: `mergedData = { ...existingData, ...admitted }`, with\n * NO version/sequence guard. So a STALE snapshot landing after a FRESH one would\n * revert every key the stale payload still holds an older value for. Coalescing\n * to a single in-flight request removes MOST of this hazard (we never fire seq 5\n * and seq 6 concurrently), but two residual races remain by construction:\n * 1. a RETRY of an older payload could still be superseded — we defend against\n * this by ALWAYS retrying the *current* latest snapshot, never the exact\n * bytes that failed (see `retry`), so a retry can never carry stale data.\n * 2. a page-exit BEACON can fire while a normal `fetch` is still in flight —\n * the beacon carries the latest snapshot, but the older in-flight fetch\n * could land AFTER the beacon on the server and revert keys.\n * To let a future server honor ordering we attach a monotonically increasing\n * `seq` (client sequence number) to every payload. The CURRENT server ignores\n * unknown body fields, so this is inert today but makes the wire self-describing;\n * a one-line server guard (`if (incoming.seq <= session.data.__seq) skip`) would\n * close race #2 without any further client change. See report for the exact\n * server change (described, NOT made — server code is read-only here).\n *\n * ── FLUSH ON EXIT ────────────────────────────────────────────────────────────\n * On `visibilitychange`→hidden and `pagehide` we send the current dirty snapshot\n * immediately. We use `navigator.sendBeacon` with a `Blob({type:'application/json'})`:\n * the endpoint needs ONLY a JSON content-type and NO credentials/custom headers\n * (identity is the `sessionId` in the body), which is exactly what sendBeacon can\n * express. We fall back to `fetch(..., {keepalive:true})` when sendBeacon is\n * unavailable or refuses (queue full). keepalive is our fallback rather than the\n * primary because sendBeacon is the purpose-built, non-blocking, unload-safe path.\n *\n * ── OFFLINE TOLERANCE ────────────────────────────────────────────────────────\n * When `navigator.onLine` is false we skip sending and re-arm on the `online`\n * event. We DO NOT persist to localStorage: all v2 funnels currently share one\n * origin (known security gap), so cross-tenant localStorage would leak one\n * tenant's answers into another's storage. In-memory only, deliberately.\n */\n\nexport interface SnapshotPersistenceOptions {\n /** Full endpoint URL for the session-data POST. */\n url: string\n /**\n * Resolve the current session id, or null if not yet established. The engine\n * won't send until this is non-null (the session anchors the write).\n */\n getSessionId: () => string | null\n /** Backoff base in ms (first retry delay). Default 1000. */\n baseDelayMs?: number\n /** Backoff cap in ms. Default 30_000. */\n maxDelayMs?: number\n /** Injectable clock for tests (defaults to real setTimeout/clearTimeout). */\n timers?: {\n setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>\n clearTimeout: (h: ReturnType<typeof setTimeout>) => void\n }\n /** Injectable RNG for jitter (defaults to Math.random). Tests can pin it. */\n random?: () => number\n}\n\ntype Snapshot = Record<string, unknown>\n\nexport interface SnapshotPersistence {\n /**\n * Hand the engine the latest full-state snapshot. Coalesces: replaces any\n * pending snapshot; if a request is in flight it becomes the next pending.\n */\n push(snapshot: Snapshot): void\n /**\n * Send the current dirty snapshot NOW via sendBeacon/keepalive (page exit).\n * Safe to call with nothing dirty (no-op). Idempotent.\n */\n flushBeacon(): void\n /** Tear down: cancel timers + detach listeners. No timer must outlive this. */\n dispose(): void\n}\n\nexport function createSnapshotPersistence(\n opts: SnapshotPersistenceOptions,\n): SnapshotPersistence {\n const baseDelay = opts.baseDelayMs ?? 1000\n const maxDelay = opts.maxDelayMs ?? 30_000\n const setT = opts.timers?.setTimeout ?? ((fn, ms) => setTimeout(fn, ms))\n const clearT = opts.timers?.clearTimeout ?? ((h) => clearTimeout(h))\n const rand = opts.random ?? Math.random\n\n // The single latest snapshot we still owe the server (dirty). null = clean.\n let pending: Snapshot | null = null\n // Monotonic client sequence — one per distinct snapshot handed to push().\n let seq = 0\n let pendingSeq = 0\n let inFlight = false\n let attempt = 0 // consecutive failures of the current pending payload\n let retryTimer: ReturnType<typeof setTimeout> | null = null\n let disposed = false\n // Once a 4xx is seen for a payload we log ONCE and stop hammering; a genuinely\n // new snapshot (new seq) clears it so a later legitimate write can still try.\n let fatalSeq = -1\n\n const clearRetry = () => {\n if (retryTimer !== null) {\n clearT(retryTimer)\n retryTimer = null\n }\n }\n\n const buildBody = (snapshot: Snapshot, s: number): string =>\n JSON.stringify({ sessionId: opts.getSessionId(), userData: snapshot, seq: s })\n\n /**\n * Attempt a normal (retryable) send of the current pending snapshot. We ALWAYS\n * read `pending` at send time rather than closing over a captured payload, so a\n * retry can never carry a stale snapshot — it carries whatever is latest.\n */\n const trySend = () => {\n if (disposed || inFlight || pending === null) return\n if (retryTimer !== null) return // a backoff wait is in progress\n const sessionId = opts.getSessionId()\n if (!sessionId) return // no session yet — flushVars re-arms us once it lands\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return // offline — 'online' re-arms\n if (pendingSeq === fatalSeq) return // this exact payload was rejected 4xx\n\n const snapshot = pending\n const sentSeq = pendingSeq\n const body = buildBody(snapshot, sentSeq)\n inFlight = true\n\n void fetch(opts.url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body,\n keepalive: true,\n })\n .then((res) => {\n inFlight = false\n if (disposed) return\n if (res.ok) {\n attempt = 0\n // Only clear the pending flag if nothing newer arrived mid-flight.\n if (pendingSeq === sentSeq) pending = null\n else trySend() // a newer snapshot queued during the flight — send it\n return\n }\n if (res.status >= 400 && res.status < 500) {\n // 4xx = the request itself is wrong; retrying won't help. Log once,\n // drop THIS payload, surface to authors. A newer snapshot (new seq)\n // is allowed to try again.\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[appfunnel] snapshot persist rejected ${res.status} (dropping snapshot; check session/payload)`,\n )\n }\n fatalSeq = sentSeq\n if (pendingSeq === sentSeq) pending = null\n else trySend()\n return\n }\n // 5xx — retryable.\n scheduleRetry()\n })\n .catch(() => {\n inFlight = false\n if (disposed) return\n // Network failure — retryable.\n scheduleRetry()\n })\n }\n\n const scheduleRetry = () => {\n if (disposed || pending === null) return\n // Exponential backoff with full jitter, capped. The retry timer keeps\n // running even if a NEWER snapshot replaces `pending` mid-wait — we don't\n // reset the backoff on every keystroke (that would let a flapping network\n // get hammered). The newer payload simply rides the already-scheduled retry.\n const exp = Math.min(maxDelay, baseDelay * 2 ** attempt)\n const delay = Math.round(exp * (0.5 + rand() * 0.5)) // 50%–100% of exp\n attempt++\n clearRetry()\n retryTimer = setT(() => {\n retryTimer = null\n trySend()\n }, delay)\n }\n\n const push = (snapshot: Snapshot) => {\n if (disposed) return\n pending = snapshot\n pendingSeq = ++seq\n // A brand-new snapshot is no longer covered by a prior 4xx verdict.\n if (fatalSeq !== -1 && pendingSeq !== fatalSeq) fatalSeq = -1\n // If we're mid-backoff, let the scheduled retry pick up the fresh payload;\n // if idle, send now.\n if (retryTimer === null) trySend()\n }\n\n const flushBeacon = () => {\n if (disposed || pending === null) return\n const sessionId = opts.getSessionId()\n if (!sessionId) return\n const body = buildBody(pending, pendingSeq)\n let delivered = false\n // sendBeacon: purpose-built for unload — non-blocking, survives navigation,\n // and (via a typed Blob) sends application/json with no headers/credentials,\n // which is all this endpoint needs.\n if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {\n try {\n delivered = navigator.sendBeacon(\n opts.url,\n new Blob([body], { type: 'application/json' }),\n )\n } catch {\n delivered = false\n }\n }\n if (!delivered) {\n // Fallback when sendBeacon is missing or its queue is full: keepalive fetch.\n try {\n void fetch(opts.url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body,\n keepalive: true,\n }).catch(() => {})\n delivered = true\n } catch {\n delivered = false\n }\n }\n // Treat a delivered beacon as clean — the payload is on its way. If it silently\n // fails at the OS level we can't know, but the visitor is leaving; a later\n // return re-hydrates from the last durably-stored snapshot.\n if (delivered) pending = null\n }\n\n // ── Lifecycle listeners ────────────────────────────────────────────────────\n const onHidden = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n flushBeacon()\n }\n }\n const onPageHide = () => flushBeacon()\n const onOnline = () => trySend() // reconnected — retry immediately\n\n if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {\n try {\n document.addEventListener('visibilitychange', onHidden)\n window.addEventListener('pagehide', onPageHide)\n window.addEventListener('online', onOnline)\n } catch {\n /* non-browser-ish environments */\n }\n }\n\n const dispose = () => {\n disposed = true\n clearRetry()\n pending = null\n if (typeof window !== 'undefined' && typeof window.removeEventListener === 'function') {\n try {\n document.removeEventListener('visibilitychange', onHidden)\n window.removeEventListener('pagehide', onPageHide)\n window.removeEventListener('online', onOnline)\n } catch {\n /* ignore */\n }\n }\n }\n\n return { push, flushBeacon, dispose }\n}\n","/**\n * The REAL platform tracker (phase-1 §1.4 / BREAKING-CHANGES B-1) — POSTs the taxonomy to the\n * platform's ingestion endpoint, the SAME one v1 funnels use, so v2 sessions/customers/events land\n * in the shared analytics graph with zero schema fork.\n *\n * TWO SURFACES can ingest, and they carry different routing + credentials (exactly mirroring how\n * `drivers/wire.ts` already routes checkout — one config shape, one branch, no second tracker):\n *\n * campaign → `POST {apiBase}/campaign/{campaignId}/event`\n * preview → `POST {apiBase}/v2/funnels/{funnelId}/preview-event?buildId=…&t=…`\n *\n * The preview surface is campaign-less by construction (`/preview/<buildId>` serves an unpromoted\n * build attached to no campaign) and carries its auth in the query string — the same HMAC build\n * token the renderer already required to serve the page, simply forwarded. Exactly one of\n * `campaignId` / `preview` is set.\n *\n * Built INSIDE the sealed bundle from serializable config (`MountOpts.tracking`) — the renderer\n * can only pass data across the build boundary, never live objects. Wired automatically by\n * `createFunnelTree` when tracking config is present.\n *\n * Behavior:\n * - SSR-safe: on the server (or with no config) this is never constructed — `createFunnelTree`\n * only wires it client-side; the export also guards `window` for direct users.\n * - mode:'test' DOES emit (changed 2026-07-28, DESIGN-2026-07-28-preview-sessions §10 step 4).\n * It used to fall back to the console tracker, which is what made both preview and live\n * `?af_mode=test` structurally untrackable. That short-circuit was the client standing in for a\n * quarantine the server did not yet have; the server now has it — sessions and events carry\n * `source`/`mode`, analytics counts only ('campaign','live'), and the integration/CAPI fan-out\n * default-withholds anything else. `mode:'test'` therefore rides the wire (below) so the server\n * can stamp it. Emitting without threading `mode` would be the unsafe version of this change.\n * - The FIRST event establishes the session: sends are SERIALIZED until the server returns a\n * `sessionId` (avoids N parallel first-events racing session creation), then fire-and-forget.\n * The sessionId is kept in sessionStorage (per-tab reload continuity) — durable cross-visit\n * identity is the `visitorId`'s job, not the session's.\n * - `visitorId` (bare id, SSR-injected from the af_vid cookie) rides on every event — the\n * ingestion side uses it for durable session lookup + anonymous-Customer continuity.\n * - Acquisition (utm + click-ids + fbp/fbc + referrer) is folded into the FIRST event's\n * `userData.query` bag — exactly where the v1 ingestion's `extractUtmParams`/attribution\n * upgrade logic already looks. Pushed once; never re-sent.\n * - Failures are swallowed (console.warn) — tracking must never break a funnel.\n */\nimport type { Tracker } from './tracking'\nimport type { Acquisition } from '../state/context'\nimport { createConsoleTracker, exposurePayload } from './tracking'\nimport { createSnapshotPersistence } from './snapshotPersistence'\n\n/** The string-valued, non-empty click-ids — the only kind either attribution shape carries. */\nfunction validClickIds(acq: Acquisition): [string, string][] {\n return Object.entries(acq.clickIds ?? {}).filter(\n (e): e is [string, string] => typeof e[1] === 'string' && e[1] !== '',\n )\n}\n\nexport interface PlatformTrackingConfig {\n /** Public API origin, e.g. `https://api.appfunnel.net` (no trailing slash). */\n apiBase: string\n /**\n * Campaign DB id (the ingestion route key). ABSENT on the build-preview surface, which is served\n * by build id and belongs to no campaign — `preview` below carries that surface's routing +\n * credential instead. Exactly one of the two is set.\n */\n campaignId?: string | null\n /**\n * BUILD-PREVIEW ingestion (`/preview/<buildId>`). Present → events POST to the campaign-less\n * preview endpoint, authenticated by the build's preview token. The server derives\n * ('preview','test') + `buildId` from that token — this config never asserts provenance, it only\n * routes. Only ever valid with `mode: 'test'`; the server forces it regardless.\n */\n preview?: {\n /** The build being previewed — what the token is signed over. */\n buildId: string\n /** HMAC preview token (`?t=` on the preview URL). */\n token: string\n } | null\n /** WebFunnel DB id (NOT the funnel.ts config id). */\n funnelId: string\n /** Verified bare visitor id (phase-1 §1.2), SSR-injected. */\n visitorId?: string | null\n /** Server-resolved session id (renderer looked it up by visitorId for hydration) — a fallback\n * seed; a per-tab sessionStorage id wins over it (v1 tab semantics). */\n sessionId?: string | null\n /** FUNNEL-split experiment assignment (phase-4): the renderer picked this funnel for the\n * visitor — the tracker emits the experiment.exposure (page-level exposures come from the\n * flow runtime instead). Deduped per (session, experiment) in analytics. */\n experiment?: { id: string; variant: string; version?: number }\n /** Campaign ROUTE that served this first request (campaign-routing pivot): the renderer's\n * serve-time evaluator matched this route → its id rides every event as `campaignRouteId`, and\n * ingestion STAMPS it once at session create (WebFunnelSession.campaignRouteId — a snapshot, like\n * the experiment exposure). Absent for the default funnel / no route matched. */\n route?: { id: string }\n mode?: 'test' | 'live'\n}\n\nfunction acquisitionQueryBag(acq: Acquisition): Record<string, string> {\n const q: Record<string, string> = {}\n const utm = acq.utm ?? {}\n for (const [k, v] of Object.entries({\n utm_source: utm.source, utm_medium: utm.medium, utm_campaign: utm.campaign,\n utm_term: utm.term, utm_content: utm.content,\n })) if (v) q[k] = v\n for (const [k, v] of validClickIds(acq)) q[k] = v\n if (acq.cookies?.fbp) q._fbp = acq.cookies.fbp\n if (acq.cookies?.fbc) q._fbc = acq.cookies.fbc\n return q\n}\n\n/**\n * The `adAttribution` body field in v1's EXACT shape (admin collectAdAttribution): click-ids by\n * their param names + `fbp`/`fbc`/`ttp` cookie values, with `fbc` synthesized from `fbclid` when\n * the cookie is absent (`fb.1.<ts>.<fbclid>` — Meta's documented format). Ingestion's existing\n * first-write-wins merge + traffic-source upgrade + typed-column extraction (phase-1 §1.3) all\n * key on these names — reusing the battle-tested path instead of forking it.\n */\nfunction acquisitionAdAttribution(acq: Acquisition): Record<string, string> {\n const a: Record<string, string> = {}\n for (const [k, v] of validClickIds(acq)) a[k] = v\n if (acq.cookies?.fbp) a.fbp = acq.cookies.fbp\n if (acq.cookies?.fbc) a.fbc = acq.cookies.fbc\n else if (acq.clickIds?.fbclid) a.fbc = `fb.1.${Date.now()}.${acq.clickIds.fbclid}`\n return a\n}\n\n/**\n * The ONE place a tracker's endpoints are decided — the twin of `drivers/wire.ts`'s `endpoint()`.\n *\n * - campaign → `{apiBase}/campaign/{campaignId}/event{path}`\n * - preview → `{apiBase}/v2/funnels/{funnelId}/preview-event{path}?buildId=…&t=…`\n *\n * The preview surface carries its auth in the QUERY STRING (build id + HMAC token) because it is\n * reached with the same forwarded `?t=` the page was served with, and because `navigator.sendBeacon`\n * — which the snapshot-persistence engine uses on page exit — cannot set custom headers.\n */\nfunction ingestEndpoint(cfg: PlatformTrackingConfig, path = ''): string {\n const base = cfg.apiBase.replace(/\\/+$/, '')\n if (cfg.preview) {\n const auth = `?buildId=${encodeURIComponent(cfg.preview.buildId)}&t=${encodeURIComponent(cfg.preview.token)}`\n return `${base}/v2/funnels/${encodeURIComponent(cfg.funnelId)}/preview-event${path}${auth}`\n }\n return `${base}/campaign/${encodeURIComponent(cfg.campaignId ?? '')}/event${path}`\n}\n\n/**\n * The per-tab sessionStorage key holding the resolved session id.\n *\n * SCOPED BY SURFACE **AND** MODE. All three of (live campaign, QA-on-prod, build preview) can be\n * opened in the same tab against the same funnel, and the server will now mint a DIFFERENT session\n * row for each — the fingerprint folds provenance in. One shared key would have each surface hand\n * the next one a session id it is not allowed to continue; the server rejects the mismatch and\n * re-mints (so nothing corrupts), but the tab would churn a fresh row on every switch.\n *\n * ⚠️ The live-campaign key is `af_sid:{campaignId}` VERBATIM — unchanged from before this surface\n * existed, so an in-flight production visitor's tab keeps its session across the deploy.\n */\nfunction sessionStorageKey(cfg: PlatformTrackingConfig): string {\n if (cfg.preview) return `af_sid:preview:${cfg.preview.buildId}`\n if (cfg.mode === 'test') return `af_sid:test:${cfg.campaignId}`\n return `af_sid:${cfg.campaignId}`\n}\n\nexport function createPlatformTracker(cfg: PlatformTrackingConfig): Tracker {\n // Never on the server. Test mode DOES emit now — see the header note: the quarantine moved from\n // \"the client refuses to speak\" to \"the server stamps mode and every reader excludes it\", and\n // `mode:'test'` is threaded onto the wire below so the server can do exactly that.\n if (typeof window === 'undefined') return createConsoleTracker()\n\n const url = ingestEndpoint(cfg)\n // Capture the RAW landing URL once, at construction — BEFORE any client-side navigation mutates\n // location.href — so it's the URL the visitor actually arrived on. Persisted verbatim (query string\n // included; PII deliberately NOT stripped — owner decision). Rides every event's userData.landingUrl\n // (below); ingestion first-writes it into session.data and reads it as the per-visit touch fallback.\n const landingUrl = window.location.href\n const sidKey = sessionStorageKey(cfg)\n let sessionId: string | null = null\n try { sessionId = window.sessionStorage.getItem(sidKey) } catch { /* storage may be blocked */ }\n if (!sessionId && cfg.sessionId) sessionId = cfg.sessionId // server-resolved (visitorId) fallback\n\n let acquisition: Acquisition | null = null\n let acquisitionSent = false\n let chain: Promise<void> = Promise.resolve()\n\n const send = async (event: string, data?: Record<string, unknown>, userData?: Record<string, unknown>) => {\n // Landing URL rides EVERY event (not just the first): the server records an attribution touch on\n // whichever request carries a changed signal and reads userData.landingUrl as the per-visit\n // fallback. Spread ours FIRST so an explicit caller-provided landingUrl still wins.\n let mergedUserData: Record<string, unknown> | undefined = landingUrl ? { landingUrl, ...userData } : userData\n let adAttribution: Record<string, string> | undefined\n // Acquisition (click-ids/UTMs) is the ONLY carrier of paid-attribution for v2 and rides the\n // FIRST event. `attachingAcquisition` marks that THIS send is carrying it — we only burn the\n // once-flag on SUCCESS (below), so a dropped first POST re-attaches on the next event instead\n // of reporting a paid session as direct (adversarial review P2).\n let attachingAcquisition = false\n if (acquisition && !acquisitionSent) {\n attachingAcquisition = true\n const query = acquisitionQueryBag(acquisition)\n // Fold onto mergedUserData (keeps landingUrl) rather than re-deriving from userData.\n if (Object.keys(query).length) mergedUserData = { ...mergedUserData, query }\n const ad = acquisitionAdAttribution(acquisition)\n if (Object.keys(ad).length) adAttribution = ad\n }\n const body = {\n // Undefined on the preview surface (campaign-less) — JSON.stringify drops the key entirely,\n // and the preview endpoint's schema has no place to put it.\n campaignId: cfg.campaignId ?? undefined,\n funnelId: cfg.funnelId,\n // RENDER MODE, ONE-WAY (§8.1). Emitted ONLY when it is 'test'; a live campaign event carries\n // no `mode` key at all, so its request body is BYTE-IDENTICAL to what shipped before this\n // change. The server mirrors the asymmetry — it accepts the literal 'test' and resolves\n // everything else (absent, malformed, explicit 'live') to 'live' — so the wire can only ever\n // DOWNGRADE a session out of production reporting, never claim its way in.\n // Inert on the preview surface: that endpoint derives ('preview','test') from the verified\n // build token and ignores the body entirely on this axis.\n ...(cfg.mode === 'test' ? { mode: 'test' as const } : {}),\n event,\n sessionId: sessionId ?? undefined,\n visitorId: cfg.visitorId ?? undefined,\n // Campaign route stamp (campaign-routing pivot): ingestion reads this ONLY at session create\n // (WebFunnelSession.campaignRouteId, snapshot semantics) — carried on every event but ignored\n // once the session exists. Undefined ⇒ default funnel / no route matched.\n campaignRouteId: cfg.route?.id ?? undefined,\n eventId: typeof data?.eventId === 'string' ? (data.eventId as string) : undefined,\n data: data ?? {},\n metadata: {\n language: typeof navigator !== 'undefined' ? navigator.language : undefined,\n screen: typeof window.screen !== 'undefined' ? `${window.screen.width}x${window.screen.height}` : undefined,\n timezone: (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return undefined } })(),\n referrer: typeof document !== 'undefined' && document.referrer ? document.referrer : undefined,\n },\n ...(mergedUserData ? { userData: mergedUserData } : {}),\n ...(adAttribution ? { adAttribution } : {}),\n }\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n keepalive: true, // survives page.exit / unload\n })\n // Only NOW commit the once-flag — a throw above (network drop) leaves it false so the next\n // event re-carries acquisition. keepalive caps the body at ~64KB; acquisition + one event is\n // far under that (variable snapshots use the separate session-data endpoint).\n if (attachingAcquisition) acquisitionSent = true\n const json = (await res.json().catch(() => null)) as { sessionId?: string } | null\n if (json?.sessionId && json.sessionId !== sessionId) {\n sessionId = json.sessionId\n try { window.sessionStorage.setItem(sidKey, sessionId) } catch { /* best-effort */ }\n }\n }\n\n const enqueue = (event: string, data?: Record<string, unknown>, userData?: Record<string, unknown>) => {\n const task = () =>\n send(event, data, userData).catch((e) => {\n if (process.env.NODE_ENV !== 'production') console.warn(`[appfunnel] track failed (${event}):`, e)\n })\n if (!sessionId) {\n // Serialize until the first response resolves the session — later events ride the chain.\n chain = chain.then(task)\n } else {\n void task()\n }\n }\n\n // ── Variable persistence (phase-1 §1.1: the server-backed namespace store) ──\n // Every namespace write pushes the FULL flat snapshot ({'user.email', 'responses.goal', …});\n // we debounce trailing-edge (the 800ms write-rate governor) and hand the latest snapshot to\n // the DURABLE persistence engine, which coalesces + retries with backoff + flushes on exit\n // (snapshotPersistence.ts). The engine POSTs to the v1 session-data endpoint (the API merges\n // it into session.data and runs the normalizer's v2 extraction). Needs a session — the engine\n // reads sessionId lazily via getSessionId and won't send until it lands.\n const VARS_DEBOUNCE_MS = 800\n // Same surface branch as the event endpoint (`ingestEndpoint`): `…/event/session/data` on a\n // campaign, `…/preview-event/session/data?buildId=…&t=…` on a build preview.\n const dataUrl = ingestEndpoint(cfg, '/session/data')\n let pendingVars: Record<string, unknown> | null = null\n let varsTimer: ReturnType<typeof setTimeout> | null = null\n\n const persistence = createSnapshotPersistence({\n url: dataUrl,\n getSessionId: () => sessionId,\n })\n\n const flushVars = () => {\n if (varsTimer) { clearTimeout(varsTimer); varsTimer = null }\n if (!pendingVars) return\n const snapshot = pendingVars\n pendingVars = null\n // Hand off to the durability engine. If the session hasn't resolved yet the\n // engine holds the snapshot and sends once getSessionId() returns non-null;\n // but to un-block that first send the moment the session lands, we also nudge\n // the engine from the send chain (a fresh push re-triggers trySend).\n persistence.push(snapshot)\n if (!sessionId) chain = chain.then(() => { persistence.push(snapshot) })\n }\n\n // On page exit the 800ms debounce may still be holding the freshest snapshot.\n // Drain it into the engine synchronously, THEN trigger the engine's beacon — so\n // the last keystroke isn't lost even though the engine's own exit listeners may\n // have already fired (listeners run in registration order; the engine's were\n // registered first, before pendingVars was drained). flushBeacon is idempotent,\n // so the redundant call when nothing changed is a no-op.\n const drainOnExit = () => {\n if (pendingVars) {\n flushVars()\n persistence.flushBeacon()\n }\n }\n try {\n window.addEventListener('visibilitychange', drainOnExit)\n window.addEventListener('pagehide', drainOnExit)\n } catch { /* non-browser-ish environments */ }\n\n // FUNNEL-split exposure: the renderer made the assignment server-side; record it once per\n // page load (analytics dedupes by session+experiment) so the split's arms are measurable.\n if (cfg.experiment?.id && cfg.experiment.variant) {\n enqueue('experiment.exposure', exposurePayload(cfg.experiment.id, cfg.experiment.variant, cfg.experiment.version))\n }\n\n return {\n track: (event: string, data?: Record<string, unknown>, userData?: Record<string, unknown>) =>\n enqueue(event, data, userData),\n identify: (email: string, eventId?: string) =>\n enqueue('user.registered', { email, ...(eventId ? { eventId } : {}) }),\n setVariables: (variables: Record<string, unknown>) => {\n pendingVars = variables // full snapshot — latest wins\n if (varsTimer) clearTimeout(varsTimer)\n varsTimer = setTimeout(flushVars, VARS_DEBOUNCE_MS)\n },\n setAcquisition: (acq: Acquisition) => { acquisition = acq },\n // The session id AS IT STANDS NOW — `null` until the first event response mints one. The\n // checkout ctx reads this per attempt (flow/mount.tsx): a first-time visitor arrives with no\n // server-resolved session, so anything that snapshots this at mount charges with `sessionId:\n // null` forever, which is precisely how off-session upsells came to fail for new buyers while\n // working for returning ones.\n getSessionId: () => sessionId,\n }\n}\n","/**\n * The **mount contract** — `mount.tsx` is THE v2 convention for a funnel build.\n *\n * Every published funnel ships a `mount.tsx` that exports exactly two things\n * (the {@link MountModule} shape):\n *\n * - `pages: RuntimePage[]` — the code-split page list in file order (the\n * admin editor rewrites this array; the anchor\n * `export const pages: RuntimePage[] = [` must survive verbatim);\n * - `tree: (opts: MountOpts) => ReactNode` — built with\n * {@link createFunnelTree}, consumed identically by the server entry (SSR)\n * and the client entry (hydration) so both render the same tree.\n *\n * `createFunnelTree` packages the canonical provider stack so funnel builds\n * don't hand-roll it:\n *\n * AssetsProvider → FunnelProvider{config, offerings, locale, mode}\n * → FunnelView{pages, layout, initialKey, prefetch:'off'}\n *\n * (`prefetch: 'off'` because the renderer owns chunk warming via\n * `<link rel=\"modulepreload\">` over `useNavigation().nextCandidates`.)\n */\n\nimport {\n createContext,\n createElement,\n Fragment,\n useContext,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport { FunnelProvider } from '../state/namespaces'\nimport { FunnelView, EmbedBridge, type RuntimePage } from './flow'\nimport { syncFunnelUrl } from './url'\nimport type { FunnelDefinition } from './spine'\nimport type { OfferingInput } from '../commerce/money'\nimport { CheckoutResume, type CheckoutDriver, type CheckoutDriverContext } from '../commerce/checkout'\nimport type { RuntimeExperiment } from './experiments'\nimport type { MessageCatalog } from '../i18n/translation'\nimport { createPlatformTracker } from '../tracking/platformTracker'\n\n// ── Assets ───────────────────────────────────────────────\n\n/**\n * Funnel asset map: logical name (e.g. `logo.svg`) → content-addressed CDN URL,\n * injected by the renderer. Assets live in a SHARED content-addressed store\n * (`appfunnel/assets/<hash>`), so identical content is stored once across all\n * builds and funnels while staying immutable. Both SSR and client read the same\n * injected map → URLs match. Defaults to `{}` (no provider needed in tests).\n */\nexport const AssetsContext = createContext<Record<string, string>>({})\n\n/** Provides the funnel's asset map (mounted for you by {@link createFunnelTree}). */\nexport function AssetsProvider({\n assets,\n children,\n}: {\n assets?: Record<string, string>\n children: ReactNode\n}): ReactNode {\n return createElement(AssetsContext.Provider, { value: assets ?? {} }, children)\n}\n\n/** The whole asset map (logical name → CDN URL). `{}` outside a provider. */\nexport function useAssets(): Record<string, string> {\n return useContext(AssetsContext)\n}\n\n/** `useAsset('logo.svg')` — one asset's CDN URL, or `undefined` if not injected. */\nexport function useAsset(name: string): string | undefined {\n return useContext(AssetsContext)[name]\n}\n\n// ── Mount options ────────────────────────────────────────\n\n/** What the renderer injects when mounting a funnel build (SSR and hydration). */\nexport interface MountOpts {\n /** Platform-resolved offering prices (one currency per visitor). */\n offerings?: OfferingInput[]\n /** Explicit locale override (URL/param/geo); wins over the detected device locale. */\n locale?: string\n mode?: 'test' | 'live'\n /** Deep-link / reload entry page — a page KEY or its URL SLUG (honored only if its entry\n * guard passes). */\n initialKey?: string\n /**\n * The campaign's URL path (`/get-started`, or `/de/get-started` with a locale prefix).\n * Present → the SDK keeps the address bar on the live page (`<basePath>/<slug>`, bare basePath on\n * the start page) via `history.replaceState`, so a refresh/share/restore resumes where the visitor\n * is instead of restarting the funnel. Set by the renderer's LIVE route; absent on embedded\n * surfaces (editor preview, tests, bare mounts), which must not touch the host URL.\n */\n basePath?: string\n /** Asset map: logical name → content-addressed CDN URL. */\n assets?: Record<string, string>\n /** Per-locale message catalogs (`messages/<locale>.json`) — overrides the mount's baked-in ones. */\n messages?: MessageCatalog\n /** The SIGNED anonymous visitor id (bare id, verified server-side; phase-1 §1.2) — the\n * renderer mints/reads the `af_vid` cookie and injects the value here. Feeds\n * `context.identity.visitorId` (experiment bucketing fallback seed, session continuity). */\n visitorId?: string | null\n /** Known customer id (identified visitor) — the preferred bucketing seed. */\n customerId?: string | null\n /** Platform tracking config (phase-1 §1.4). Present → `createFunnelTree` wires the real\n * platform tracker (events POST to the shared ingestion endpoint). Absent (tests, bare mounts,\n * the editor's in-browser preview) → the console tracker. Serializable on purpose — it crosses\n * the sealed-build boundary as data. */\n tracking?: {\n apiBase: string\n /** Campaign DB id — the campaign ingestion route key. Absent on the build-preview surface,\n * which belongs to no campaign; `preview` carries that surface's routing instead. Exactly one\n * of the two is set. */\n campaignId?: string | null\n /** BUILD-PREVIEW ingestion (`/preview/<buildId>`, DESIGN-2026-07-28-preview-sessions §10 step 4).\n * Present → the tracker POSTs to the campaign-less preview endpoint with the build's preview\n * token. The SERVER derives ('preview','test') + buildId from that token; this only routes. */\n preview?: {\n /** The build being previewed — what the token is signed over. */\n buildId: string\n /** The HMAC preview token the renderer already required to serve this page. */\n token: string\n } | null\n /** WebFunnel DB id (NOT the funnel.ts config id). */\n funnelId: string\n /** FUNNEL-split assignment (phase-4) — the tracker emits its exposure event. */\n experiment?: { id: string; variant: string; version?: number }\n /** Campaign ROUTE that served this first request (campaign-routing pivot) — the tracker forwards\n * its id as `campaignRouteId` so ingestion stamps the session once at create. */\n route?: { id: string }\n }\n /**\n * BUILD-PREVIEW checkout config (`/preview/<buildId>`). Present → `createFunnelTree` wires the\n * REAL provider driver against the campaign-less preview endpoint, so an author can exercise an\n * actual test-mode checkout on an unpublished build instead of the mock.\n *\n * Deliberately separate from `tracking`: charging and ingesting are independent capabilities on\n * this surface (a deployment with no PREVIEW_TOKEN_SECRET can do neither, one with no\n * TRACKING_API_BASE can do neither — but the renderer decides each on its own facts). Set only by\n * the renderer's preview route; a live funnel never carries it. Serializable — it crosses the\n * sealed-build boundary as data.\n *\n * NOTE (2026-07-28): a preview no longer stays out of *ingestion* — it writes ('preview','test')\n * sessions via `tracking.preview`, which analytics excludes. It still stays out of every REPORT\n * and out of the integration/CAPI fan-out, which is what \"a preview must not pollute shared data\"\n * always actually meant.\n */\n previewCheckout?: {\n apiBase: string\n /** WebFunnel DB id (NOT the funnel.ts config id). */\n funnelId: string\n /** The build being previewed — what the token is signed over. */\n buildId: string\n /** The HMAC preview token the renderer already required to serve this page. */\n token: string\n }\n /** Server-hydrated namespace snapshot (phase-1 §1.1): flat namespaced keys\n * (`user.email`, `responses.goal`, …) from the visitor's latest session — the renderer\n * resolves it by visitorId so a returning visitor resumes with their answers. */\n sessionValues?: Record<string, string | number | boolean | null | string[]>\n /** RUNNING page-experiment records (phase-4): platform-owned wiring over the bundle's\n * `@variant` pages, injected at render time — no rebuild to start/stop/reweight. */\n experiments?: RuntimeExperiment[]\n /** The server-resolved session id backing `sessionValues` — seeds the tracker (fallback)\n * and `context.session.id`. */\n sessionId?: string | null\n /** Mount the cross-origin {@link EmbedBridge} — the Funnel Embed API (flow snapshot + goTo/goNext/\n * goPrevious + screenshot, over the versioned wire protocol). Set ONLY by preview surfaces (the live\n * editor + the sealed `/preview/<buildId>` build); a live prod funnel leaves it off so it can't be\n * driven / can't leak its flow state to an embedder. */\n embed?: boolean\n /** FLASH-FREE HYDRATION (SSR-isolation Option B): set by the CLIENT entry only. The initial\n * render is seeded with DEFAULTS (matching the build-time default-state prerender it hydrates\n * against), then `sessionValues` are applied post-hydration via `store.setMany` — a returning\n * visitor's saved answers pop into already-visible controls, no empty flash, no mismatch. Unset\n * on SSR (the server render must bake sessionValues into the markup, the original behavior). */\n deferSessionValues?: boolean\n /** SSR-isolation Option B, FIX 1: render `initialKey` VERBATIM without evaluating its entry guard.\n * Set by the build-time PRERENDER stage so a guarded page (paywall/upsell) prerenders its OWN\n * default-state markup (the renderer decides target-vs-bounce at serve time by evaluating the\n * guard against the visitor's session). Defaults to `deferSessionValues` (the client trusts the\n * server-resolved key); the prerender sets it true explicitly while leaving sessionValues baked. */\n trustInitialKey?: boolean\n}\n\n/**\n * The shape a funnel build's `mount.tsx` module must export — the named,\n * versioned contract between a build and the renderer/editor:\n * `import type { MountModule } from '@appfunnel-dev/sdk'`.\n */\nexport type MountModule = {\n pages: RuntimePage[]\n tree: (opts: MountOpts) => ReactNode\n}\n\n// ── createFunnelTree ─────────────────────────────────────\n\nexport interface CreateFunnelTreeInput {\n /** The funnel's spine (`defineFunnel(...)`). */\n config: FunnelDefinition\n /** Code-split pages in file order (the same array `mount.tsx` exports). */\n pages: RuntimePage[]\n /** Persistent chrome wrapped around the page area (mounted once). */\n layout?: ComponentType<{ children: ReactNode }>\n /**\n * The platform checkout driver factory — `mount.tsx` imports it from a driver\n * subpath entry (`import { stripeCheckoutDriver } from '@appfunnel-dev/sdk/driver-stripe'`)\n * and passes it here, so the provider choice stays in the SDK-owned authoring\n * contract while the ctx (apiBase/campaign/funnel/mode/identity) stays\n * renderer-injected. Called once per `tree()` mount, only when tracking config\n * is present — without it there is no API to charge against (preview, tests,\n * bare mounts) and the mock driver fallback stays in place. Test mode still\n * constructs the real driver: the wire contract carries `mode`, and the server\n * resolves the provider's TEST store from it (that's the quarantine, not the client).\n */\n checkoutDriver?: (ctx: CheckoutDriverContext) => CheckoutDriver\n /**\n * The funnel's message catalogs (`messages/<locale>.json`), keyed by locale. The generated\n * `mount.tsx` imports the catalogs and passes them here so `t()` resolves at render (SSR + client);\n * `MountOpts.messages` can still override per-request. Omit for funnels with no translations.\n */\n messages?: MessageCatalog\n}\n\n/**\n * Build a funnel's `tree(opts)` — the standard provider stack over your pages.\n *\n * ```tsx\n * export const pages: RuntimePage[] = [ ... ]\n * export const tree = createFunnelTree({ config, pages, layout: Layout })\n * ```\n */\nexport function createFunnelTree({\n config,\n pages,\n layout,\n checkoutDriver,\n messages,\n}: CreateFunnelTreeInput): (opts: MountOpts) => ReactNode {\n return function tree(opts: MountOpts): ReactNode {\n const mode = opts.mode ?? 'live'\n const embedEnabled = opts.embed ?? false\n // Narrowed once so the onNavigate closure below stays a plain string capture.\n const basePath = opts.basePath\n // The real platform tracker when the renderer passed tracking config (the factory itself\n // no-ops for SSR; since 2026-07-28 it no longer no-ops for test mode — `mode` rides the wire\n // and the server stamps it). One instance per tree() call: the entries\n // call tree() exactly once per mount (SSR render / hydration), so no memo hook is needed\n // and tree() stays callable outside React.\n const tracker = opts.tracking\n ? createPlatformTracker({\n ...opts.tracking,\n visitorId: opts.visitorId,\n sessionId: opts.sessionId,\n mode,\n })\n : undefined\n // The platform checkout driver when mount.tsx exported a factory AND the renderer passed\n // tracking config — the driver ctx derives from the SAME injected data the tracker uses,\n // so there is no second config channel. Unlike the tracker (whose factory no-ops on the\n // server), the driver IS constructed during SSR, deliberately: factories are\n // construction-safe closures over serializable ctx (provider SDKs load only in\n // effects/event handlers), and a driver's renderInline returns the same stable placeholder\n // on the server and the first client paint. Skipping construction server-side would SSR\n // the mock fallback's <button> into inline surfaces — a guaranteed hydration mismatch.\n // TWO surfaces can charge, and they carry different routing + credentials:\n // - `tracking` → live/campaign traffic, campaign-scoped endpoint;\n // - `previewCheckout` → an unpublished build being QA'd, campaign-less endpoint + the\n // build's preview token. Test mode only (the server re-asserts it).\n // Neither present (editor preview, tests, bare mounts) ⇒ no API to charge against, and the\n // mock driver fallback stays in place. A CAMPAIGN `tracking` config wins if both were somehow\n // supplied — a funnel serving real traffic must never be routed through the preview endpoint.\n // Keyed on `campaignId` specifically, not on `tracking` being present at all: since 2026-07-28\n // the preview surface ALSO passes `tracking` (campaign-less, `tracking.preview`), and routing\n // its checkout through the campaign branch would produce an empty campaign id in the URL.\n // THE SESSION IS READ PER ATTEMPT, not snapshotted at mount. `opts.sessionId` is only what the\n // SERVER could resolve from the visitor cookie, which is `null` for anyone arriving for the\n // first time; the real id is minted by the first event response and lives on the tracker. A\n // getter is what makes the two agree: an upsell that fires minutes after the paywall reads the\n // session that exists BY THEN, instead of the nothing that existed at mount.\n //\n // This is not cosmetic. `runStripeUpsell` refuses an upsell with no `sessionId` (\"An upsell\n // needs the funnel session of a previous purchase\") and the error is retryable, so the SDK\n // opened its recovery sheet and asked for a card — turning the one-tap off-session charge into\n // a card form, for FIRST-TIME BUYERS ONLY. Returning visitors carried a server-resolved id and\n // worked, which is exactly why it survived testing.\n const liveSessionId = (): string | null | undefined => tracker?.getSessionId?.() ?? opts.sessionId\n const checkoutCtx: CheckoutDriverContext | undefined = opts.tracking?.campaignId\n ? {\n apiBase: opts.tracking.apiBase,\n campaignId: opts.tracking.campaignId,\n funnelId: opts.tracking.funnelId,\n mode,\n get sessionId() {\n return liveSessionId()\n },\n visitorId: opts.visitorId,\n }\n : opts.previewCheckout\n ? {\n apiBase: opts.previewCheckout.apiBase,\n funnelId: opts.previewCheckout.funnelId,\n preview: { buildId: opts.previewCheckout.buildId, token: opts.previewCheckout.token },\n mode,\n get sessionId() {\n return liveSessionId()\n },\n visitorId: opts.visitorId,\n }\n : undefined\n const checkout = checkoutDriver && checkoutCtx ? checkoutDriver(checkoutCtx) : undefined\n // The redirect-return resume leg (hosted-checkout success URLs carry\n // `?af_checkout={correlationId}`) rides the persistent layout slot: it needs\n // tracker + navigation, which only exist INSIDE FunnelView's nav context, and\n // the layout is the one mount-once spot there. SSR-safe (renders null;\n // effect-only) and inert without the param.\n const layoutWithResume: ComponentType<{ children: ReactNode }> = function LayoutWithCheckoutResume({\n children,\n }: {\n children: ReactNode\n }): ReactNode {\n return createElement(\n Fragment,\n null,\n createElement(CheckoutResume, null),\n embedEnabled ? createElement(EmbedBridge, null) : null,\n layout ? createElement(layout, null, children) : children,\n )\n }\n return createElement(AssetsProvider, {\n assets: opts.assets,\n children: createElement(FunnelProvider, {\n config,\n offerings: opts.offerings ?? [],\n messages: opts.messages ?? messages,\n locale: opts.locale,\n tracker,\n checkout,\n sessionValues: opts.sessionValues,\n deferSessionValues: opts.deferSessionValues,\n experiments: opts.experiments,\n context: {\n mode,\n visitorId: opts.visitorId ?? null,\n customerId: opts.customerId ?? null,\n sessionId: opts.sessionId ?? null,\n },\n children: createElement(FunnelView, {\n pages,\n layout: layoutWithResume,\n initialKey: opts.initialKey,\n // FLASH-FREE HYDRATION (Option B, FIX 1): trust the SERVER-resolved initialKey instead of\n // re-running the entry guard. The CLIENT sets this via deferSessionValues (its deferred\n // snapshot is empty, so re-running the guard would bounce a qualified deep-link and\n // mismatch the markup). The build-time PRERENDER sets `trustInitialKey` explicitly so a\n // guarded page prerenders its OWN default-state markup (the renderer picks target-vs-bounce\n // at serve time). Live SSR fallback leaves both off → the guard runs against real values.\n trustInitialKey: opts.trustInitialKey ?? opts.deferSessionValues,\n prefetch: 'off', // renderer background-modulepreloads all chunks\n // URL PATH SYNC: keep the address bar on the live page whenever the renderer told us the\n // campaign's path. Without it the URL freezes at the entry and a refresh restarts the\n // funnel. Effect-only (never runs during SSR) and absent on embedded surfaces.\n onNavigate: basePath\n ? (_key, slug, isStart) => syncFunnelUrl(basePath, slug, isStart)\n : undefined,\n }),\n }),\n })\n }\n}\n","import {\n createElement,\n type ButtonHTMLAttributes,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from 'react'\nimport { useField } from '../state/namespaces'\nimport { useNavigation } from './flow'\n\n/**\n * The two-tier component model (doc 07 §\"magic ≠ black box\"): the magic lives in\n * headless hooks (`useField`, `useNavigation`); these styleable components are\n * thin, restyle-able wrappers over them. Every `<Choice>` reduces to `useField`\n * + your own JSX — eject any time. They pass `className`/DOM props straight\n * through and never dictate a pixel beyond the minimum.\n */\n\nexport interface ChoiceOption {\n value: string\n label?: ReactNode\n}\n\nexport interface ChoiceProps {\n /** Writable namespaced path to bind, e.g. `responses.goal`. */\n bind: string\n /** The selectable options (a bare string is shorthand for `{value, label}`). */\n options: (string | ChoiceOption)[]\n /** Class applied to each option button. */\n className?: string\n /** Class applied to the selected option button (in addition to `className`). */\n selectedClassName?: string\n /** Advance navigation after a choice (handy for single-tap question pages). */\n advanceOnSelect?: boolean\n}\n\n/**\n * Single-select bound to a writable field. `<Choice bind=\"responses.goal\"\n * options={[\"lose\", \"gain\"]} />` — selecting persists + (later) tracks; pass\n * `advanceOnSelect` to move to the next page on tap.\n */\nexport function Choice({\n bind,\n options,\n className,\n selectedClassName,\n advanceOnSelect,\n}: ChoiceProps): ReactNode {\n const { value, set } = useField(bind)\n const nav = useNavigation()\n\n return options.map((opt) => {\n const o: ChoiceOption = typeof opt === 'string' ? { value: opt } : opt\n const selected = value === o.value\n return createElement(\n 'button',\n {\n key: o.value,\n type: 'button',\n 'aria-pressed': selected,\n 'data-selected': selected || undefined,\n className: selected && selectedClassName ? `${className ?? ''} ${selectedClassName}`.trim() : className,\n onClick: () => {\n set(o.value)\n if (advanceOnSelect) nav.next()\n },\n },\n o.label ?? o.value,\n )\n })\n}\n\nexport interface NextProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n children?: ReactNode\n}\n\n/** A button that advances the funnel (the current page's `next`, else linear). */\nexport function Next({ children, onClick, ...rest }: NextProps): ReactNode {\n const { next } = useNavigation()\n return createElement(\n 'button',\n {\n type: 'button',\n ...rest,\n onClick: (e: ReactMouseEvent<HTMLButtonElement>) => {\n onClick?.(e)\n if (!e.defaultPrevented) next()\n },\n },\n children ?? 'Continue',\n )\n}\n\nexport interface BackProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n children?: ReactNode\n}\n\n/** A button that returns to the previous page; hidden when there's no history. */\nexport function Back({ children, onClick, ...rest }: BackProps): ReactNode {\n const { back, canGoBack } = useNavigation()\n if (!canGoBack) return null\n return createElement(\n 'button',\n {\n type: 'button',\n ...rest,\n onClick: (e: ReactMouseEvent<HTMLButtonElement>) => {\n onClick?.(e)\n if (!e.defaultPrevented) back()\n },\n },\n children ?? 'Back',\n )\n}\n","/**\n * WHICH STORE — the only question the funnel's last screen can actually answer.\n *\n * The funnel's terminal page has to send the visitor to the App Store or Google Play, and it has\n * to pick without asking them. That is what this module does, and deliberately all it does.\n *\n * WHY THERE IS NO PARAMETER MACHINERY HERE. An earlier version of this module built a URL out of\n * the funnel's identity, answers and ad click-ids and appended it to the store link. That is\n * theatre: the App Store DISCARDS every query parameter, so on iOS — 85.7% of this product's\n * traffic — nothing appended ever reaches the app. Google Play carries exactly one (`referrer`).\n * A component whose headline feature works on neither store as advertised is worse than one that\n * doesn't claim it, so the claim is gone.\n *\n * Carrying identity across an install is a real problem with real solutions — a universal link\n * (only if the app is already installed), a deferred-deeplink vendor (probabilistic fingerprint\n * matching), or joining on identity inside the app. None of them are a query string on a store\n * URL, and none of them belong in a button. If the destination is a tracker link that does its own\n * routing, pass it as `fallback` and it will be used for everyone.\n */\n\nimport type { FunnelContext } from '../state/context'\n\n/**\n * The store platform, collapsed to what a download button can act on. `other` is desktop and\n * anything unrecognised — a real destination in its own right (a QR page, the marketing site),\n * not an error state.\n */\nexport type AppPlatform = 'ios' | 'android' | 'other'\n\n/**\n * The platform for a funnel context. Reads `context.os.name` rather than sniffing the user-agent\n * again: the SDK has exactly one detector ({@link ../state/context}.osFromUserAgent) and a second\n * one drifting alongside it is how a funnel ends up sending iPhones to Google Play.\n *\n * That detector was wrong until 2026-07-31 — every iOS user-agent contains \"like Mac OS X\", so the\n * mac test matched first and reported every iPhone as macOS. Replaying all 144,938 stored sessions\n * put 85.7% of them in the wrong bucket. This function is only as good as that fix, which is why\n * the fix has its own test file pinned against real user-agents.\n */\nexport function platformFor(context: FunnelContext): AppPlatform {\n\tconst os = context.os?.name\n\tif (os === 'iOS') return 'ios'\n\tif (os === 'Android') return 'android'\n\treturn 'other'\n}\n\n/** Where each platform goes. Every field optional — give the stores you actually have. */\nexport interface AppStores {\n\t/** App Store URL (or a universal link / tracker link, if that is your iOS destination). */\n\tios?: string\n\t/** Google Play URL (or a universal link / tracker link). */\n\tandroid?: string\n\t/**\n\t * Desktop and anything unrecognised — a QR page, the marketing site, or a single tracker link\n\t * that does its own platform routing.\n\t */\n\tfallback?: string\n}\n\n/** `scheme:` at the head — `https:`, `myapp:`, anything. */\nconst HAS_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/\n\n/**\n * Normalise a destination: a bare host/path gets `https://`. Already-schemed URLs,\n * protocol-relative (`//cdn…`), absolute paths (`/thanks`) and bare `?`/`#` are returned as given.\n */\nexport function withScheme(url: string): string {\n\tconst t = url.trim()\n\tif (!t) return ''\n\tif (HAS_SCHEME.test(t)) return t\n\tif (t.startsWith('//') || t.startsWith('/') || t.startsWith('?') || t.startsWith('#')) return t\n\treturn `https://${t}`\n}\n\n/**\n * Pick the store URL for a platform, specific → generic.\n *\n * The `other` chain ends in a store URL rather than nothing on purpose: a desktop visitor landing\n * on the App Store's web listing is a mildly wrong destination, whereas a dead button on the page\n * they just paid on is a refund.\n */\nexport function resolveStore(stores: AppStores, platform: AppPlatform): string {\n\tconst ordered =\n\t\tplatform === 'ios'\n\t\t\t? [stores.ios, stores.fallback, stores.android]\n\t\t\t: platform === 'android'\n\t\t\t\t? [stores.android, stores.fallback, stores.ios]\n\t\t\t\t: [stores.fallback, stores.ios, stores.android]\n\treturn withScheme(ordered.find((v) => v && v.trim()) ?? '')\n}\n","/**\n * The React half of the app download — {@link useAppDownload} (headless) and\n * {@link DownloadButton} (the styleable wrapper over it), following the same two-tier model as\n * `useField`/`<Choice>`: the decision lives in the hook, the component is a thin `<a>` that passes\n * every DOM prop straight through and dictates no pixel.\n *\n * TEST MODE DOES NOT NAVIGATE. Under `mode: 'test'` — the editor preview and `?af_mode=test` QA —\n * the store URL is logged to the console and the click still tracks, but the browser stays put. A\n * preview that navigated to the App Store on the finish page would tear the editor's iframe out\n * from under the author and end every automated preview walk one page early.\n *\n * NO STORE-BADGE COMPONENT, deliberately. The SDK cannot style a badge that matches an arbitrary\n * template's design tokens, and every ready-made component added here is a permanent public-API\n * commitment (see ../elements/index.ts). Both badges side by side is this component twice, with\n * the template's own markup as children.\n */\n\nimport {\n\tcreateElement,\n\tuseCallback,\n\tuseRef,\n\ttype AnchorHTMLAttributes,\n\ttype MouseEvent as ReactMouseEvent,\n\ttype ReactNode,\n} from 'react'\nimport { useFunnel } from '../state/namespaces'\nimport { platformFor, resolveStore, type AppPlatform, type AppStores } from './store'\n\n/** Where the store page opens. Same values as the HTML attribute, and passed through as one. */\nexport type DownloadTarget = '_self' | '_blank'\n\n/**\n * `usePlatform()` — `'ios' | 'android' | 'other'` for the current visitor. The store decision on\n * its own, so copy and artwork can agree with where the button goes\n * (`platform === 'android' ? 'Get it on Google Play' : 'Download on the App Store'`).\n */\nexport function usePlatform(): AppPlatform {\n\treturn platformFor(useFunnel().context)\n}\n\nexport interface UseAppDownloadResult {\n\t/** The visitor's platform — which of `ios`/`android`/`fallback` was chosen. */\n\tplatform: AppPlatform\n\t/** The store URL for that platform, `https://`-normalised. Empty if nothing was configured. */\n\turl: string\n\t/** Track the click and go. Returns the URL; in test mode it logs instead of navigating. */\n\topen: (target?: DownloadTarget) => string\n}\n\n/** What {@link useAppDownload} accepts on top of the store URLs. */\nexport interface DownloadTracking {\n\t/**\n\t * Analytics event emitted on click, defaulting to `download.click` with `{ url, platform }`.\n\t * Pass a string to rename it, or `false` to emit nothing.\n\t */\n\tevent?: string | false\n\t/** Where the store opens. Default `_self`. */\n\ttarget?: DownloadTarget\n}\n\n/**\n * The headless download — everything {@link DownloadButton} does, without the markup. Reach for it\n * when the handoff isn't a link: a redirect after a countdown, a QR code's payload, a button that\n * has to do other things first.\n *\n * @example\n * const app = useAppDownload({\n * ios: 'https://apps.apple.com/app/id1234567890',\n * android: 'https://play.google.com/store/apps/details?id=com.acme.app',\n * })\n * return <button onClick={() => app.open()}>Download the app</button>\n */\nexport function useAppDownload(\n\tstores: AppStores & DownloadTracking\n): UseAppDownloadResult {\n\tconst funnel = useFunnel()\n\t// Props arrive as a fresh object literal every render; a ref keeps `open` stable without\n\t// asking callers to memoise.\n\tconst latest = useRef(stores)\n\tlatest.current = stores\n\n\tconst platform = platformFor(funnel.context)\n\tconst url = resolveStore(stores, platform)\n\n\tconst open = useCallback(\n\t\t(target?: DownloadTarget) => {\n\t\t\tconst s = latest.current\n\t\t\tconst p = platformFor(funnel.context)\n\t\t\tconst to = resolveStore(s, p)\n\t\t\tif (!to) return ''\n\t\t\tif (s.event !== false) funnel.track(s.event ?? 'download.click', { url: to, platform: p })\n\t\t\tif (funnel.context.system.mode === 'test') {\n\t\t\t\tconsole.info('[appfunnel] app download (test mode — not navigating):', to)\n\t\t\t\treturn to\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tif ((target ?? s.target ?? '_self') === '_blank')\n\t\t\t\t\twindow.open(to, '_blank', 'noopener,noreferrer')\n\t\t\t\telse window.location.href = to\n\t\t\t} catch {\n\t\t\t\t// Sandboxed frames can refuse top-level navigation. The click was still tracked;\n\t\t\t\t// swallowing keeps a blocked handoff from crashing the finish page.\n\t\t\t}\n\t\t\treturn to\n\t\t},\n\t\t[funnel]\n\t)\n\n\treturn { platform, url, open }\n}\n\nexport interface DownloadButtonProps\n\textends AppStores,\n\t\tDownloadTracking,\n\t\tOmit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'target'> {\n\tchildren?: ReactNode\n}\n\n/**\n * A link to the app store, picked from the visitor's device.\n *\n * iPhones and iPads get `ios`, Android phones get `android`, everyone else gets `fallback` — so\n * the finish page is one button rather than a row of badges the visitor has to choose between.\n * Renders a plain `<a href=\"{store url}\">`: real link semantics, keyboard-focusable, and styleable\n * entirely through `className`/`style` like any anchor.\n *\n * Calling `preventDefault()` in your own `onClick` cancels the navigation, matching `<Next>`/`<Back>`.\n *\n * @example\n * <DownloadButton\n * ios=\"https://apps.apple.com/app/id1234567890\"\n * android=\"https://play.google.com/store/apps/details?id=com.acme.app\"\n * fallback=\"https://acme.com/get-the-app\"\n * className=\"btn-primary\"\n * >\n * Download the app\n * </DownloadButton>\n *\n * @example\n * // Both badges side by side — the same component twice, wrapping the template's own markup.\n * <DownloadButton ios={appStoreUrl} fallback={appStoreUrl} target=\"_blank\"><AppStoreBadge /></DownloadButton>\n * <DownloadButton android={playUrl} fallback={playUrl} target=\"_blank\"><GooglePlayBadge /></DownloadButton>\n */\nexport function DownloadButton({\n\tios,\n\tandroid,\n\tfallback,\n\tevent,\n\ttarget = '_self',\n\tchildren,\n\tonClick,\n\t...rest\n}: DownloadButtonProps): ReactNode {\n\tconst app = useAppDownload({ ios, android, fallback, event, target })\n\treturn createElement(\n\t\t'a',\n\t\t{\n\t\t\t...rest,\n\t\t\thref: app.url || undefined,\n\t\t\ttarget,\n\t\t\t// A `_blank` link without this leaves the opened page a handle on the funnel window.\n\t\t\trel: rest.rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined),\n\t\t\tonClick: (e: ReactMouseEvent<HTMLAnchorElement>) => {\n\t\t\t\tonClick?.(e)\n\t\t\t\tif (e.defaultPrevented) return\n\t\t\t\t// The anchor already points at the right store, so a plain navigation would work.\n\t\t\t\t// Going through `open` instead is what makes the click trackable and keeps the\n\t\t\t\t// editor preview from walking itself out of the funnel.\n\t\t\t\te.preventDefault()\n\t\t\t\tapp.open(target)\n\t\t\t},\n\t\t},\n\t\tchildren ?? 'Download the app'\n\t)\n}\n","/**\n * The **event catalog** — the single source of truth for *which* events exist and\n * how each is routed (analytics vs. integrations vs. runtime-only). Pairs with the\n * typed payloads in {@link ./tracking}.`FunnelEventDataMap`.\n *\n * This replaces the duplication the monorepo flags (`shared/events/catalog.ts` is\n * \"documentation, not imported\" — the taxonomy is re-declared in\n * `admin/lib/funnel/tracking.ts`, each `admin/lib/integrations/*` EVENT_MAP, and\n * `api/src/integrations/constants`). In v2 the **SDK owns the taxonomy** (it emits\n * it), so the bus, the pixel loaders, and the platform all import *this*.\n */\n\nexport type EventSource = 'frontend' | 'backend'\n\nexport interface EventMeta {\n /** Where the event originates. `backend` = the server emits it (e.g. a settled charge). */\n sources: EventSource[]\n /** Persisted to analytics. */\n tracking: boolean\n /** Forwarded to ad/integration pixels (Meta/GTM/TikTok/…). */\n integrations: boolean\n /** Runtime-only JS subscription — not persisted, not sent to integrations. */\n runtime?: boolean\n}\n\n/**\n * Every event the system knows, with its routing metadata. Keys are the taxonomy\n * names (a superset of `FunnelEventDataMap`: it also lists the **backend-originated**\n * monetary events the platform pushes to the client bus for pixel firing).\n */\nexport const EVENT_CATALOG = {\n // ── Runtime / navigation (JS subscriptions; not persisted) ──\n 'funnel.start': { sources: ['frontend'], tracking: false, integrations: false, runtime: true },\n 'experiment.exposure': { sources: ['frontend'], tracking: true, integrations: false },\n\n // ── Frontend, tracked; some also go to pixels ──\n 'page.view': { sources: ['frontend'], tracking: true, integrations: true },\n 'page.exit': { sources: ['frontend'], tracking: true, integrations: false },\n 'user.registered': { sources: ['frontend'], tracking: true, integrations: true },\n 'checkout.start': { sources: ['frontend'], tracking: true, integrations: true },\n 'checkout.payment_added': { sources: ['frontend'], tracking: true, integrations: true },\n 'checkout.failed': { sources: ['frontend'], tracking: true, integrations: false },\n // A charge the platform refused because the buyer already owns the product. `integrations:\n // false` is the load-bearing flag: the flow advances as if the purchase succeeded, but no\n // money moved on THIS attempt, so nothing may reach an ad pixel — the original purchase\n // already fired its Purchase/Subscribe with the server-minted dedup id.\n 'checkout.already_purchased': { sources: ['frontend'], tracking: true, integrations: false },\n\n // ── Backend-originated money events (server mints the eventId; client fires the pixel) ──\n 'purchase.complete': { sources: ['backend'], tracking: true, integrations: true },\n 'customer.first_purchase': { sources: ['backend'], tracking: true, integrations: true },\n 'subscription.created': { sources: ['backend'], tracking: true, integrations: true },\n 'subscription.renewal': { sources: ['backend'], tracking: true, integrations: true },\n} as const satisfies Record<string, EventMeta>\n\n/** A catalog event name. */\nexport type CatalogEvent = keyof typeof EVENT_CATALOG\n\n/** True if the event is persisted to analytics. */\nexport function isTrackableEvent(name: string): boolean {\n return EVENT_CATALOG[name as CatalogEvent]?.tracking ?? false\n}\n\n/** True if the event is forwarded to ad/integration pixels. */\nexport function isIntegrationEvent(name: string): boolean {\n return EVENT_CATALOG[name as CatalogEvent]?.integrations ?? false\n}\n\n/** The events that feed integrations — the set a pixel loader subscribes to. */\nexport function integrationEvents(): CatalogEvent[] {\n return (Object.keys(EVENT_CATALOG) as CatalogEvent[]).filter((e) => EVENT_CATALOG[e].integrations)\n}\n","/**\n * `<Script>` — the **declared third-party embed** primitive (phase-2 §2.3d).\n *\n * The controlled escape hatch for runtime widgets/embeds that genuinely need\n * client code — Intercom, Trustpilot, a calendar embed, an operator pixel. Unlike\n * arbitrary `<script>` injection, it's **declared** (the funnel author writes\n * `<Script src=…/>`), **deduped** (injected at most once per id/src, even across\n * page navigation in the SPA — a chat widget must not reload on every page), and\n * **CSP-friendly** (accepts a `nonce`).\n *\n * CSP enforcement itself is the **renderer's** job: it scans the funnel's declared\n * `<Script src>`s (build-time) into the `script-src` allowlist and supplies the\n * `nonce`. This component just injects the tag at the right time.\n *\n * For *analytics* pixels, prefer subscribing to the `window.appfunnel` bus\n * ({@link ../tracking/bus}) — `<Script>` is for non-analytics third-party widgets\n * (and the pixel loader snippets, which then subscribe to the bus).\n */\n\nimport { useEffect } from 'react'\n\n/** Module-level registry so a given script injects once for the app's lifetime. */\nconst injected = new Set<string>()\n\nexport interface ScriptProps {\n /** The third-party script URL. */\n src: string\n /**\n * When to inject. `afterInteractive` (default) injects on mount (after hydration);\n * `lazyOnload` waits for idle/`load` so it never competes with the funnel's own JS.\n */\n strategy?: 'afterInteractive' | 'lazyOnload'\n /** Dedup key (defaults to `src`) — the script injects at most once per id. */\n id?: string\n /** Defaults to `true`. */\n async?: boolean\n defer?: boolean\n /** CSP nonce — supplied by the renderer. */\n nonce?: string\n /** Extra attributes (e.g. `data-*`). */\n attributes?: Record<string, string>\n onLoad?: () => void\n onError?: () => void\n}\n\n/**\n * Injects a third-party script once. Renders nothing. Persists across navigation\n * (the tag is never removed on unmount — third-party widgets should stay loaded).\n */\nexport function Script({\n src,\n strategy = 'afterInteractive',\n id,\n async = true,\n defer,\n nonce,\n attributes,\n onLoad,\n onError,\n}: ScriptProps): null {\n useEffect(() => {\n if (typeof document === 'undefined') return\n const key = id ?? src\n if (injected.has(key)) {\n onLoad?.() // already present from an earlier mount\n return\n }\n\n const inject = () => {\n if (injected.has(key)) return\n injected.add(key)\n const el = document.createElement('script')\n el.src = src\n if (id) el.id = id\n if (async) el.async = true\n if (defer) el.defer = true\n if (nonce) el.nonce = nonce\n if (attributes) for (const [k, v] of Object.entries(attributes)) el.setAttribute(k, v)\n el.addEventListener('load', () => onLoad?.())\n el.addEventListener('error', () => {\n injected.delete(key) // allow a later retry\n onError?.()\n })\n document.head.appendChild(el)\n }\n\n if (strategy === 'lazyOnload') {\n const ric = (window as unknown as { requestIdleCallback?: (cb: () => void) => number })\n .requestIdleCallback\n if (ric) {\n ric(inject)\n } else {\n const t = setTimeout(inject, 0)\n return () => clearTimeout(t)\n }\n return\n }\n inject()\n // src/id identify the script; injection is idempotent via the registry.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [src, id])\n\n return null\n}\n\n/** Internal: clear the injected-script registry (tests only). */\nexport function __resetScriptRegistry(): void {\n injected.clear()\n}\n"]}
1
+ {"version":3,"sources":["../src/state/measurementSystem.ts","../src/flow/url.ts","../src/tracking/snapshotPersistence.ts","../src/tracking/platformTracker.ts","../src/flow/mount.tsx","../src/flow/components.tsx","../src/download/store.ts","../src/download/DownloadButton.tsx","../src/tracking/catalog.ts","../src/embed/script.tsx"],"names":["useLocale","useState","useEffect","createConsoleTracker","exposurePayload","createContext","createElement","useContext","Fragment","CheckoutResume","EmbedBridge","FunnelProvider","FunnelView","useField","useNavigation","useFunnel","useRef","useCallback"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `useMeasurementSystem()` — which unit system the current visitor most likely\n * *expects* (imperial ft/lbs vs metric cm/kg). A funnel seeds its own unit toggle\n * from it, e.g. BetterMe's height/weight steps default to ft/lbs for a US visitor\n * and cm/kg elsewhere.\n *\n * ## What it reports (and what it does NOT)\n * This hook reports an **expectation**, derived from where the visitor is / what\n * their browser advertises. It does **not** store the user's choice — that stays\n * funnel-owned (e.g. `data.units`). Intended use is one-shot seeding of local state:\n *\n * ```ts\n * const initialUnits = useMeasurementSystem() // 'imperial' | 'metric'\n * const [units, setUnits] = useState(initialUnits)\n * ```\n *\n * ## Detection sources, in priority order\n * 1. **Snapshot context region** — `useLocale().region` (the `context.locale`\n * region subtag). When the renderer injects a server-consistent locale (see\n * `buildContext` overrides), this is the country/region code; otherwise it is\n * the region subtag `detect()` reads from `navigator.language` on the client.\n * 2. **`navigator.language` region subtag** — direct fallback (e.g. `en-US` → `US`)\n * for the case where the context region is empty but the browser advertises one.\n * 3. **`'metric'`** — the final fallback (the world default, and the SSR default).\n *\n * ## The rule\n * Imperial is returned for the only countries that are *fully* imperial — the\n * United States (`US`), Liberia (`LR`), and Myanmar (`MM`) — **plus the United\n * Kingdom (`GB`)**. Everything else (and anything unknown) is metric.\n *\n * ### The UK decision (deliberate)\n * The UK is officially metric but, for **body measurements specifically**, everyday\n * usage is imperial-leaning: people give height in feet/inches and bodyweight in\n * stones/pounds. Since this hook exists to seed *body-measurement* toggles (height/\n * weight), `GB` returns `'imperial'` — the default a UK visitor is most likely to\n * expect on a weight/height step. The return type stays binary on purpose; a funnel\n * that wants UK-specific **stones** handling (which imperial-vs-metric can't express)\n * should branch on the country itself (`useLocale().region === 'GB'`) rather than on\n * this hook.\n *\n * ## SSR / hydration safety\n * Prerendered shells are rendered on the server with **no visitor context** (region\n * is empty server-side, real only on the client), so reading the region during\n * render would diverge between the server HTML and the first client paint and cause\n * a hydration mismatch. Following the codebase contract (\"seed from injected data,\n * never client-side detection on first paint\"), this hook returns `'metric'` for the\n * first render on **both** sides — a byte-clean hydration — then resolves the real\n * system in a **post-mount effect** and updates. Funnels seeding local state from it\n * therefore start metric and flip to imperial for imperial-region visitors on the\n * first commit (imperceptible, and correct once settled).\n */\n\nimport { useEffect, useState } from 'react'\nimport { useLocale } from './namespaces'\n\nexport type MeasurementSystem = 'imperial' | 'metric'\n\n/**\n * ISO-3166-1 alpha-2 codes that seed an imperial default. `US`/`LR`/`MM` are the\n * only fully-imperial countries; `GB` is added deliberately for body measurements\n * (see the module JSDoc — UK bodyweight/height are conventionally imperial).\n */\nconst IMPERIAL_COUNTRIES = new Set(['US', 'LR', 'MM', 'GB'])\n\n/**\n * Pure rule: map a country/region code to a measurement system. Returns `null` when\n * the input is absent or not a 2-letter code (so the caller can fall through to the\n * next source); a *known* metric country returns `'metric'`, not `null`.\n */\nexport function measurementSystemForCountry(\n\tcountry: string | null | undefined,\n): MeasurementSystem | null {\n\tif (!country) return null\n\tconst cc = country.trim().toUpperCase()\n\tif (!/^[A-Z]{2}$/.test(cc)) return null\n\treturn IMPERIAL_COUNTRIES.has(cc) ? 'imperial' : 'metric'\n}\n\n/** The region subtag of `navigator.language` (e.g. `en-US` → `US`), or undefined. Client-only. */\nfunction navigatorRegion(): string | undefined {\n\tif (typeof navigator === 'undefined') return undefined\n\tconst tag = navigator.language\n\tif (!tag) return undefined\n\tconst parts = tag.split('-')\n\tif (parts.length < 2) return undefined // language-only tag, no region\n\tconst region = parts[parts.length - 1]\n\treturn /^[A-Za-z]{2}$/.test(region) ? region : undefined\n}\n\n/**\n * Pure resolver over the priority chain: context region → `navigator.language`\n * region → `'metric'`. Reads `navigator` (client-only) — call it in an effect, not\n * during render (see the hook). Exported for direct/unit use.\n */\nexport function resolveMeasurementSystem(\n\tcontextRegion?: string | null,\n): MeasurementSystem {\n\treturn (\n\t\tmeasurementSystemForCountry(contextRegion) ??\n\t\tmeasurementSystemForCountry(navigatorRegion()) ??\n\t\t'metric'\n\t)\n}\n\n/**\n * Seed a funnel's unit toggle from the visitor's likely-expected measurement system.\n * SSR-safe: `'metric'` on first paint (server + hydration), resolved post-mount. See\n * the module JSDoc for the rule, the UK decision, and the detection priority.\n */\nexport function useMeasurementSystem(): MeasurementSystem {\n\t// Read during render (safe — not used for first-paint output) to feed the effect\n\t// and re-resolve if the injected region ever changes.\n\tconst region = useLocale().region\n\tconst [system, setSystem] = useState<MeasurementSystem>('metric')\n\tuseEffect(() => {\n\t\tsetSystem(resolveMeasurementSystem(region))\n\t}, [region])\n\treturn system\n}\n","/**\n * URL PATH SYNC — the funnel's live page in the address bar.\n *\n * Funnel navigation is a React state swap, not a document navigation, so nothing moves the URL on\n * its own: without this the visitor walks 30 screens while the address bar still reads the campaign\n * entry, and a refresh (or a share, or a browser restore) drops them back at page one.\n *\n * The renderer injects the campaign's path (`MountOpts.basePath`, e.g. `/get-started` or\n * `/de/get-started`) and the SDK writes `<basePath>/<page slug>` on every page change. The\n * START page keeps the bare campaign URL — that is the canonical ad destination, and a reload there\n * means \"start over\", which is exactly what the bare URL does.\n *\n * `replaceState`, never `pushState` (v0 behavior, deliberate): funnel back is in-memory and\n * branch-aware, so browser Back must stay an EXIT from the funnel rather than an untracked jump\n * backwards through it. Query (utm/click ids) and hash survive every rewrite.\n */\n\n/** Rewrite the address bar to the live page. No-op on the server, without a basePath, or if unchanged. */\nexport function syncFunnelUrl(basePath: string, slug: string, isStart: boolean): void {\n\tif (typeof window === 'undefined' || !basePath) return\n\tconst base = basePath.replace(/\\/+$/, '')\n\t// Encode per SEGMENT: a slug may legitimately contain slashes (funnels that mirror a source\n\t// site's nested paths), and those must stay real path separators — percent-encoding them would\n\t// produce a URL the renderer can't map back to the page.\n\tconst path = isStart\n\t\t? base || '/'\n\t\t: `${base}/${slug.split('/').map(encodeURIComponent).join('/')}`\n\tconst { search, hash, pathname } = window.location\n\tif (path === pathname) return\n\ttry {\n\t\twindow.history.replaceState(window.history.state, '', `${path}${search}${hash}`)\n\t} catch {\n\t\t// Sandboxed/cross-origin-restricted frames can throw on history writes — the funnel still runs.\n\t}\n}\n","/**\n * Durable snapshot persistence engine (audit fix: fire-and-forget → durable).\n *\n * The v2 namespace store's \"biggest magic\" is server-backed, cross-device answer\n * durability: every `responses.set(...)` / `user.set(...)` pushes the FULL flat\n * snapshot, which the tracker debounces (800ms, leading governor in\n * platformTracker) and POSTs to the v1 session-data endpoint. Before this engine\n * the POST was fire-and-forget: no retry, no queue, no flush-on-exit, no offline\n * tolerance — a single failed POST silently lost the visitor's answers, breaking\n * the \"your progress survives a reload\" promise.\n *\n * This engine sits BEHIND the 800ms debounce (the debounce stays the write-rate\n * governor; the engine is the durability layer). It owns one endpoint: sending\n * the latest full-state snapshot, coalesced, with retry/backoff, and a\n * synchronous flush on page exit.\n *\n * ── LATEST-SNAPSHOT-WINS ─────────────────────────────────────────────────────\n * Snapshots are full-state (the entire flat store), so ONLY THE LATEST MATTERS.\n * We never queue a backlog — at most one request is in flight and at most one\n * snapshot is pending. A newer snapshot arriving while one is in flight simply\n * REPLACES the pending payload; it is never appended.\n *\n * ── OUT-OF-ORDER / VERSION GUARD ─────────────────────────────────────────────\n * The server (apps/api …/session/data → FunnelTrackingController.updateSessionData)\n * merges each POST per-key: `mergedData = { ...existingData, ...admitted }`, with\n * NO version/sequence guard. So a STALE snapshot landing after a FRESH one would\n * revert every key the stale payload still holds an older value for. Coalescing\n * to a single in-flight request removes MOST of this hazard (we never fire seq 5\n * and seq 6 concurrently), but two residual races remain by construction:\n * 1. a RETRY of an older payload could still be superseded — we defend against\n * this by ALWAYS retrying the *current* latest snapshot, never the exact\n * bytes that failed (see `retry`), so a retry can never carry stale data.\n * 2. a page-exit BEACON can fire while a normal `fetch` is still in flight —\n * the beacon carries the latest snapshot, but the older in-flight fetch\n * could land AFTER the beacon on the server and revert keys.\n * To let a future server honor ordering we attach a monotonically increasing\n * `seq` (client sequence number) to every payload. The CURRENT server ignores\n * unknown body fields, so this is inert today but makes the wire self-describing;\n * a one-line server guard (`if (incoming.seq <= session.data.__seq) skip`) would\n * close race #2 without any further client change. See report for the exact\n * server change (described, NOT made — server code is read-only here).\n *\n * ── FLUSH ON EXIT ────────────────────────────────────────────────────────────\n * On `visibilitychange`→hidden and `pagehide` we send the current dirty snapshot\n * immediately. We use `navigator.sendBeacon` with a `Blob({type:'application/json'})`:\n * the endpoint needs ONLY a JSON content-type and NO credentials/custom headers\n * (identity is the `sessionId` in the body), which is exactly what sendBeacon can\n * express. We fall back to `fetch(..., {keepalive:true})` when sendBeacon is\n * unavailable or refuses (queue full). keepalive is our fallback rather than the\n * primary because sendBeacon is the purpose-built, non-blocking, unload-safe path.\n *\n * ── OFFLINE TOLERANCE ────────────────────────────────────────────────────────\n * When `navigator.onLine` is false we skip sending and re-arm on the `online`\n * event. We DO NOT persist to localStorage: all v2 funnels currently share one\n * origin (known security gap), so cross-tenant localStorage would leak one\n * tenant's answers into another's storage. In-memory only, deliberately.\n */\n\nexport interface SnapshotPersistenceOptions {\n /** Full endpoint URL for the session-data POST. */\n url: string\n /**\n * Resolve the current session id, or null if not yet established. The engine\n * won't send until this is non-null (the session anchors the write).\n */\n getSessionId: () => string | null\n /** Backoff base in ms (first retry delay). Default 1000. */\n baseDelayMs?: number\n /** Backoff cap in ms. Default 30_000. */\n maxDelayMs?: number\n /** Injectable clock for tests (defaults to real setTimeout/clearTimeout). */\n timers?: {\n setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>\n clearTimeout: (h: ReturnType<typeof setTimeout>) => void\n }\n /** Injectable RNG for jitter (defaults to Math.random). Tests can pin it. */\n random?: () => number\n}\n\ntype Snapshot = Record<string, unknown>\n\nexport interface SnapshotPersistence {\n /**\n * Hand the engine the latest full-state snapshot. Coalesces: replaces any\n * pending snapshot; if a request is in flight it becomes the next pending.\n */\n push(snapshot: Snapshot): void\n /**\n * Send the current dirty snapshot NOW via sendBeacon/keepalive (page exit).\n * Safe to call with nothing dirty (no-op). Idempotent.\n */\n flushBeacon(): void\n /** Tear down: cancel timers + detach listeners. No timer must outlive this. */\n dispose(): void\n}\n\nexport function createSnapshotPersistence(\n opts: SnapshotPersistenceOptions,\n): SnapshotPersistence {\n const baseDelay = opts.baseDelayMs ?? 1000\n const maxDelay = opts.maxDelayMs ?? 30_000\n const setT = opts.timers?.setTimeout ?? ((fn, ms) => setTimeout(fn, ms))\n const clearT = opts.timers?.clearTimeout ?? ((h) => clearTimeout(h))\n const rand = opts.random ?? Math.random\n\n // The single latest snapshot we still owe the server (dirty). null = clean.\n let pending: Snapshot | null = null\n // Monotonic client sequence — one per distinct snapshot handed to push().\n let seq = 0\n let pendingSeq = 0\n let inFlight = false\n let attempt = 0 // consecutive failures of the current pending payload\n let retryTimer: ReturnType<typeof setTimeout> | null = null\n let disposed = false\n // Once a 4xx is seen for a payload we log ONCE and stop hammering; a genuinely\n // new snapshot (new seq) clears it so a later legitimate write can still try.\n let fatalSeq = -1\n\n const clearRetry = () => {\n if (retryTimer !== null) {\n clearT(retryTimer)\n retryTimer = null\n }\n }\n\n const buildBody = (snapshot: Snapshot, s: number): string =>\n JSON.stringify({ sessionId: opts.getSessionId(), userData: snapshot, seq: s })\n\n /**\n * Attempt a normal (retryable) send of the current pending snapshot. We ALWAYS\n * read `pending` at send time rather than closing over a captured payload, so a\n * retry can never carry a stale snapshot — it carries whatever is latest.\n */\n const trySend = () => {\n if (disposed || inFlight || pending === null) return\n if (retryTimer !== null) return // a backoff wait is in progress\n const sessionId = opts.getSessionId()\n if (!sessionId) return // no session yet — flushVars re-arms us once it lands\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return // offline — 'online' re-arms\n if (pendingSeq === fatalSeq) return // this exact payload was rejected 4xx\n\n const snapshot = pending\n const sentSeq = pendingSeq\n const body = buildBody(snapshot, sentSeq)\n inFlight = true\n\n void fetch(opts.url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body,\n keepalive: true,\n })\n .then((res) => {\n inFlight = false\n if (disposed) return\n if (res.ok) {\n attempt = 0\n // Only clear the pending flag if nothing newer arrived mid-flight.\n if (pendingSeq === sentSeq) pending = null\n else trySend() // a newer snapshot queued during the flight — send it\n return\n }\n if (res.status >= 400 && res.status < 500) {\n // 4xx = the request itself is wrong; retrying won't help. Log once,\n // drop THIS payload, surface to authors. A newer snapshot (new seq)\n // is allowed to try again.\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[appfunnel] snapshot persist rejected ${res.status} (dropping snapshot; check session/payload)`,\n )\n }\n fatalSeq = sentSeq\n if (pendingSeq === sentSeq) pending = null\n else trySend()\n return\n }\n // 5xx — retryable.\n scheduleRetry()\n })\n .catch(() => {\n inFlight = false\n if (disposed) return\n // Network failure — retryable.\n scheduleRetry()\n })\n }\n\n const scheduleRetry = () => {\n if (disposed || pending === null) return\n // Exponential backoff with full jitter, capped. The retry timer keeps\n // running even if a NEWER snapshot replaces `pending` mid-wait — we don't\n // reset the backoff on every keystroke (that would let a flapping network\n // get hammered). The newer payload simply rides the already-scheduled retry.\n const exp = Math.min(maxDelay, baseDelay * 2 ** attempt)\n const delay = Math.round(exp * (0.5 + rand() * 0.5)) // 50%–100% of exp\n attempt++\n clearRetry()\n retryTimer = setT(() => {\n retryTimer = null\n trySend()\n }, delay)\n }\n\n const push = (snapshot: Snapshot) => {\n if (disposed) return\n pending = snapshot\n pendingSeq = ++seq\n // A brand-new snapshot is no longer covered by a prior 4xx verdict.\n if (fatalSeq !== -1 && pendingSeq !== fatalSeq) fatalSeq = -1\n // If we're mid-backoff, let the scheduled retry pick up the fresh payload;\n // if idle, send now.\n if (retryTimer === null) trySend()\n }\n\n const flushBeacon = () => {\n if (disposed || pending === null) return\n const sessionId = opts.getSessionId()\n if (!sessionId) return\n const body = buildBody(pending, pendingSeq)\n let delivered = false\n // sendBeacon: purpose-built for unload — non-blocking, survives navigation,\n // and (via a typed Blob) sends application/json with no headers/credentials,\n // which is all this endpoint needs.\n if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {\n try {\n delivered = navigator.sendBeacon(\n opts.url,\n new Blob([body], { type: 'application/json' }),\n )\n } catch {\n delivered = false\n }\n }\n if (!delivered) {\n // Fallback when sendBeacon is missing or its queue is full: keepalive fetch.\n try {\n void fetch(opts.url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body,\n keepalive: true,\n }).catch(() => {})\n delivered = true\n } catch {\n delivered = false\n }\n }\n // Treat a delivered beacon as clean — the payload is on its way. If it silently\n // fails at the OS level we can't know, but the visitor is leaving; a later\n // return re-hydrates from the last durably-stored snapshot.\n if (delivered) pending = null\n }\n\n // ── Lifecycle listeners ────────────────────────────────────────────────────\n const onHidden = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n flushBeacon()\n }\n }\n const onPageHide = () => flushBeacon()\n const onOnline = () => trySend() // reconnected — retry immediately\n\n if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {\n try {\n document.addEventListener('visibilitychange', onHidden)\n window.addEventListener('pagehide', onPageHide)\n window.addEventListener('online', onOnline)\n } catch {\n /* non-browser-ish environments */\n }\n }\n\n const dispose = () => {\n disposed = true\n clearRetry()\n pending = null\n if (typeof window !== 'undefined' && typeof window.removeEventListener === 'function') {\n try {\n document.removeEventListener('visibilitychange', onHidden)\n window.removeEventListener('pagehide', onPageHide)\n window.removeEventListener('online', onOnline)\n } catch {\n /* ignore */\n }\n }\n }\n\n return { push, flushBeacon, dispose }\n}\n","/**\n * The REAL platform tracker (phase-1 §1.4 / BREAKING-CHANGES B-1) — POSTs the taxonomy to the\n * platform's ingestion endpoint, the SAME one v1 funnels use, so v2 sessions/customers/events land\n * in the shared analytics graph with zero schema fork.\n *\n * TWO SURFACES can ingest, and they carry different routing + credentials (exactly mirroring how\n * `drivers/wire.ts` already routes checkout — one config shape, one branch, no second tracker):\n *\n * campaign → `POST {apiBase}/campaign/{campaignId}/event`\n * preview → `POST {apiBase}/v2/funnels/{funnelId}/preview-event?buildId=…&t=…`\n *\n * The preview surface is campaign-less by construction (`/preview/<buildId>` serves an unpromoted\n * build attached to no campaign) and carries its auth in the query string — the same HMAC build\n * token the renderer already required to serve the page, simply forwarded. Exactly one of\n * `campaignId` / `preview` is set.\n *\n * Built INSIDE the sealed bundle from serializable config (`MountOpts.tracking`) — the renderer\n * can only pass data across the build boundary, never live objects. Wired automatically by\n * `createFunnelTree` when tracking config is present.\n *\n * Behavior:\n * - SSR-safe: on the server (or with no config) this is never constructed — `createFunnelTree`\n * only wires it client-side; the export also guards `window` for direct users.\n * - mode:'test' DOES emit (changed 2026-07-28, DESIGN-2026-07-28-preview-sessions §10 step 4).\n * It used to fall back to the console tracker, which is what made both preview and live\n * `?af_mode=test` structurally untrackable. That short-circuit was the client standing in for a\n * quarantine the server did not yet have; the server now has it — sessions and events carry\n * `source`/`mode`, analytics counts only ('campaign','live'), and the integration/CAPI fan-out\n * default-withholds anything else. `mode:'test'` therefore rides the wire (below) so the server\n * can stamp it. Emitting without threading `mode` would be the unsafe version of this change.\n * - The FIRST event establishes the session: sends are SERIALIZED until the server returns a\n * `sessionId` (avoids N parallel first-events racing session creation), then fire-and-forget.\n * The sessionId is kept in sessionStorage (per-tab reload continuity) — durable cross-visit\n * identity is the `visitorId`'s job, not the session's.\n * - `visitorId` (bare id, SSR-injected from the af_vid cookie) rides on every event — the\n * ingestion side uses it for durable session lookup + anonymous-Customer continuity.\n * - Acquisition (utm + click-ids + fbp/fbc + referrer) is folded into the FIRST event's\n * `userData.query` bag — exactly where the v1 ingestion's `extractUtmParams`/attribution\n * upgrade logic already looks. Pushed once; never re-sent.\n * - Failures are swallowed (console.warn) — tracking must never break a funnel.\n */\nimport type { Tracker } from './tracking'\nimport type { Acquisition } from '../state/context'\nimport { createConsoleTracker, exposurePayload } from './tracking'\nimport { createSnapshotPersistence } from './snapshotPersistence'\n\n/** The string-valued, non-empty click-ids — the only kind either attribution shape carries. */\nfunction validClickIds(acq: Acquisition): [string, string][] {\n return Object.entries(acq.clickIds ?? {}).filter(\n (e): e is [string, string] => typeof e[1] === 'string' && e[1] !== '',\n )\n}\n\nexport interface PlatformTrackingConfig {\n /** Public API origin, e.g. `https://api.appfunnel.net` (no trailing slash). */\n apiBase: string\n /**\n * Campaign DB id (the ingestion route key). ABSENT on the build-preview surface, which is served\n * by build id and belongs to no campaign — `preview` below carries that surface's routing +\n * credential instead. Exactly one of the two is set.\n */\n campaignId?: string | null\n /**\n * BUILD-PREVIEW ingestion (`/preview/<buildId>`). Present → events POST to the campaign-less\n * preview endpoint, authenticated by the build's preview token. The server derives\n * ('preview','test') + `buildId` from that token — this config never asserts provenance, it only\n * routes. Only ever valid with `mode: 'test'`; the server forces it regardless.\n */\n preview?: {\n /** The build being previewed — what the token is signed over. */\n buildId: string\n /** HMAC preview token (`?t=` on the preview URL). */\n token: string\n } | null\n /** WebFunnel DB id (NOT the funnel.ts config id). */\n funnelId: string\n /** Verified bare visitor id (phase-1 §1.2), SSR-injected. */\n visitorId?: string | null\n /** Server-resolved session id (renderer looked it up by visitorId for hydration) — a fallback\n * seed; a per-tab sessionStorage id wins over it (v1 tab semantics). */\n sessionId?: string | null\n /** FUNNEL-split experiment assignment (phase-4): the renderer picked this funnel for the\n * visitor — the tracker emits the experiment.exposure (page-level exposures come from the\n * flow runtime instead). Deduped per (session, experiment) in analytics. */\n experiment?: { id: string; variant: string; version?: number }\n /** Campaign ROUTE that served this first request (campaign-routing pivot): the renderer's\n * serve-time evaluator matched this route → its id rides every event as `campaignRouteId`, and\n * ingestion STAMPS it once at session create (WebFunnelSession.campaignRouteId — a snapshot, like\n * the experiment exposure). Absent for the default funnel / no route matched. */\n route?: { id: string }\n mode?: 'test' | 'live'\n}\n\nfunction acquisitionQueryBag(acq: Acquisition): Record<string, string> {\n const q: Record<string, string> = {}\n const utm = acq.utm ?? {}\n for (const [k, v] of Object.entries({\n utm_source: utm.source, utm_medium: utm.medium, utm_campaign: utm.campaign,\n utm_term: utm.term, utm_content: utm.content,\n })) if (v) q[k] = v\n for (const [k, v] of validClickIds(acq)) q[k] = v\n if (acq.cookies?.fbp) q._fbp = acq.cookies.fbp\n if (acq.cookies?.fbc) q._fbc = acq.cookies.fbc\n return q\n}\n\n/**\n * The `adAttribution` body field in v1's EXACT shape (admin collectAdAttribution): click-ids by\n * their param names + `fbp`/`fbc`/`ttp` cookie values, with `fbc` synthesized from `fbclid` when\n * the cookie is absent (`fb.1.<ts>.<fbclid>` — Meta's documented format). Ingestion's existing\n * first-write-wins merge + traffic-source upgrade + typed-column extraction (phase-1 §1.3) all\n * key on these names — reusing the battle-tested path instead of forking it.\n */\nfunction acquisitionAdAttribution(acq: Acquisition): Record<string, string> {\n const a: Record<string, string> = {}\n for (const [k, v] of validClickIds(acq)) a[k] = v\n if (acq.cookies?.fbp) a.fbp = acq.cookies.fbp\n if (acq.cookies?.fbc) a.fbc = acq.cookies.fbc\n else if (acq.clickIds?.fbclid) a.fbc = `fb.1.${Date.now()}.${acq.clickIds.fbclid}`\n return a\n}\n\n/**\n * The ONE place a tracker's endpoints are decided — the twin of `drivers/wire.ts`'s `endpoint()`.\n *\n * - campaign → `{apiBase}/campaign/{campaignId}/event{path}`\n * - preview → `{apiBase}/v2/funnels/{funnelId}/preview-event{path}?buildId=…&t=…`\n *\n * The preview surface carries its auth in the QUERY STRING (build id + HMAC token) because it is\n * reached with the same forwarded `?t=` the page was served with, and because `navigator.sendBeacon`\n * — which the snapshot-persistence engine uses on page exit — cannot set custom headers.\n */\nfunction ingestEndpoint(cfg: PlatformTrackingConfig, path = ''): string {\n const base = cfg.apiBase.replace(/\\/+$/, '')\n if (cfg.preview) {\n const auth = `?buildId=${encodeURIComponent(cfg.preview.buildId)}&t=${encodeURIComponent(cfg.preview.token)}`\n return `${base}/v2/funnels/${encodeURIComponent(cfg.funnelId)}/preview-event${path}${auth}`\n }\n return `${base}/campaign/${encodeURIComponent(cfg.campaignId ?? '')}/event${path}`\n}\n\n/**\n * The per-tab sessionStorage key holding the resolved session id.\n *\n * SCOPED BY SURFACE **AND** MODE. All three of (live campaign, QA-on-prod, build preview) can be\n * opened in the same tab against the same funnel, and the server will now mint a DIFFERENT session\n * row for each — the fingerprint folds provenance in. One shared key would have each surface hand\n * the next one a session id it is not allowed to continue; the server rejects the mismatch and\n * re-mints (so nothing corrupts), but the tab would churn a fresh row on every switch.\n *\n * ⚠️ The live-campaign key is `af_sid:{campaignId}` VERBATIM — unchanged from before this surface\n * existed, so an in-flight production visitor's tab keeps its session across the deploy.\n */\nfunction sessionStorageKey(cfg: PlatformTrackingConfig): string {\n if (cfg.preview) return `af_sid:preview:${cfg.preview.buildId}`\n if (cfg.mode === 'test') return `af_sid:test:${cfg.campaignId}`\n return `af_sid:${cfg.campaignId}`\n}\n\nexport function createPlatformTracker(cfg: PlatformTrackingConfig): Tracker {\n // Never on the server. Test mode DOES emit now — see the header note: the quarantine moved from\n // \"the client refuses to speak\" to \"the server stamps mode and every reader excludes it\", and\n // `mode:'test'` is threaded onto the wire below so the server can do exactly that.\n if (typeof window === 'undefined') return createConsoleTracker()\n\n const url = ingestEndpoint(cfg)\n // Capture the RAW landing URL once, at construction — BEFORE any client-side navigation mutates\n // location.href — so it's the URL the visitor actually arrived on. Persisted verbatim (query string\n // included; PII deliberately NOT stripped — owner decision). Rides every event's userData.landingUrl\n // (below); ingestion first-writes it into session.data and reads it as the per-visit touch fallback.\n const landingUrl = window.location.href\n const sidKey = sessionStorageKey(cfg)\n let sessionId: string | null = null\n try { sessionId = window.sessionStorage.getItem(sidKey) } catch { /* storage may be blocked */ }\n if (!sessionId && cfg.sessionId) sessionId = cfg.sessionId // server-resolved (visitorId) fallback\n\n let acquisition: Acquisition | null = null\n let acquisitionSent = false\n let chain: Promise<void> = Promise.resolve()\n\n const send = async (event: string, data?: Record<string, unknown>, userData?: Record<string, unknown>) => {\n // Landing URL rides EVERY event (not just the first): the server records an attribution touch on\n // whichever request carries a changed signal and reads userData.landingUrl as the per-visit\n // fallback. Spread ours FIRST so an explicit caller-provided landingUrl still wins.\n let mergedUserData: Record<string, unknown> | undefined = landingUrl ? { landingUrl, ...userData } : userData\n let adAttribution: Record<string, string> | undefined\n // Acquisition (click-ids/UTMs) is the ONLY carrier of paid-attribution for v2 and rides the\n // FIRST event. `attachingAcquisition` marks that THIS send is carrying it — we only burn the\n // once-flag on SUCCESS (below), so a dropped first POST re-attaches on the next event instead\n // of reporting a paid session as direct (adversarial review P2).\n let attachingAcquisition = false\n if (acquisition && !acquisitionSent) {\n attachingAcquisition = true\n const query = acquisitionQueryBag(acquisition)\n // Fold onto mergedUserData (keeps landingUrl) rather than re-deriving from userData.\n if (Object.keys(query).length) mergedUserData = { ...mergedUserData, query }\n const ad = acquisitionAdAttribution(acquisition)\n if (Object.keys(ad).length) adAttribution = ad\n }\n const body = {\n // Undefined on the preview surface (campaign-less) — JSON.stringify drops the key entirely,\n // and the preview endpoint's schema has no place to put it.\n campaignId: cfg.campaignId ?? undefined,\n funnelId: cfg.funnelId,\n // RENDER MODE, ONE-WAY (§8.1). Emitted ONLY when it is 'test'; a live campaign event carries\n // no `mode` key at all, so its request body is BYTE-IDENTICAL to what shipped before this\n // change. The server mirrors the asymmetry — it accepts the literal 'test' and resolves\n // everything else (absent, malformed, explicit 'live') to 'live' — so the wire can only ever\n // DOWNGRADE a session out of production reporting, never claim its way in.\n // Inert on the preview surface: that endpoint derives ('preview','test') from the verified\n // build token and ignores the body entirely on this axis.\n ...(cfg.mode === 'test' ? { mode: 'test' as const } : {}),\n event,\n sessionId: sessionId ?? undefined,\n visitorId: cfg.visitorId ?? undefined,\n // Campaign route stamp (campaign-routing pivot): ingestion reads this ONLY at session create\n // (WebFunnelSession.campaignRouteId, snapshot semantics) — carried on every event but ignored\n // once the session exists. Undefined ⇒ default funnel / no route matched.\n campaignRouteId: cfg.route?.id ?? undefined,\n eventId: typeof data?.eventId === 'string' ? (data.eventId as string) : undefined,\n data: data ?? {},\n metadata: {\n language: typeof navigator !== 'undefined' ? navigator.language : undefined,\n screen: typeof window.screen !== 'undefined' ? `${window.screen.width}x${window.screen.height}` : undefined,\n timezone: (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return undefined } })(),\n referrer: typeof document !== 'undefined' && document.referrer ? document.referrer : undefined,\n },\n ...(mergedUserData ? { userData: mergedUserData } : {}),\n ...(adAttribution ? { adAttribution } : {}),\n }\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n keepalive: true, // survives page.exit / unload\n })\n // Only NOW commit the once-flag — a throw above (network drop) leaves it false so the next\n // event re-carries acquisition. keepalive caps the body at ~64KB; acquisition + one event is\n // far under that (variable snapshots use the separate session-data endpoint).\n if (attachingAcquisition) acquisitionSent = true\n const json = (await res.json().catch(() => null)) as { sessionId?: string } | null\n if (json?.sessionId && json.sessionId !== sessionId) {\n sessionId = json.sessionId\n try { window.sessionStorage.setItem(sidKey, sessionId) } catch { /* best-effort */ }\n }\n }\n\n const enqueue = (event: string, data?: Record<string, unknown>, userData?: Record<string, unknown>) => {\n const task = () =>\n send(event, data, userData).catch((e) => {\n if (process.env.NODE_ENV !== 'production') console.warn(`[appfunnel] track failed (${event}):`, e)\n })\n if (!sessionId) {\n // Serialize until the first response resolves the session — later events ride the chain.\n chain = chain.then(task)\n } else {\n void task()\n }\n }\n\n // ── Variable persistence (phase-1 §1.1: the server-backed namespace store) ──\n // Every namespace write pushes the FULL flat snapshot ({'user.email', 'responses.goal', …});\n // we debounce trailing-edge (the 800ms write-rate governor) and hand the latest snapshot to\n // the DURABLE persistence engine, which coalesces + retries with backoff + flushes on exit\n // (snapshotPersistence.ts). The engine POSTs to the v1 session-data endpoint (the API merges\n // it into session.data and runs the normalizer's v2 extraction). Needs a session — the engine\n // reads sessionId lazily via getSessionId and won't send until it lands.\n const VARS_DEBOUNCE_MS = 800\n // Same surface branch as the event endpoint (`ingestEndpoint`): `…/event/session/data` on a\n // campaign, `…/preview-event/session/data?buildId=…&t=…` on a build preview.\n const dataUrl = ingestEndpoint(cfg, '/session/data')\n let pendingVars: Record<string, unknown> | null = null\n let varsTimer: ReturnType<typeof setTimeout> | null = null\n\n const persistence = createSnapshotPersistence({\n url: dataUrl,\n getSessionId: () => sessionId,\n })\n\n const flushVars = () => {\n if (varsTimer) { clearTimeout(varsTimer); varsTimer = null }\n if (!pendingVars) return\n const snapshot = pendingVars\n pendingVars = null\n // Hand off to the durability engine. If the session hasn't resolved yet the\n // engine holds the snapshot and sends once getSessionId() returns non-null;\n // but to un-block that first send the moment the session lands, we also nudge\n // the engine from the send chain (a fresh push re-triggers trySend).\n persistence.push(snapshot)\n if (!sessionId) chain = chain.then(() => { persistence.push(snapshot) })\n }\n\n // On page exit the 800ms debounce may still be holding the freshest snapshot.\n // Drain it into the engine synchronously, THEN trigger the engine's beacon — so\n // the last keystroke isn't lost even though the engine's own exit listeners may\n // have already fired (listeners run in registration order; the engine's were\n // registered first, before pendingVars was drained). flushBeacon is idempotent,\n // so the redundant call when nothing changed is a no-op.\n const drainOnExit = () => {\n if (pendingVars) {\n flushVars()\n persistence.flushBeacon()\n }\n }\n try {\n window.addEventListener('visibilitychange', drainOnExit)\n window.addEventListener('pagehide', drainOnExit)\n } catch { /* non-browser-ish environments */ }\n\n // FUNNEL-split exposure: the renderer made the assignment server-side; record it once per\n // page load (analytics dedupes by session+experiment) so the split's arms are measurable.\n if (cfg.experiment?.id && cfg.experiment.variant) {\n enqueue('experiment.exposure', exposurePayload(cfg.experiment.id, cfg.experiment.variant, cfg.experiment.version))\n }\n\n return {\n track: (event: string, data?: Record<string, unknown>, userData?: Record<string, unknown>) =>\n enqueue(event, data, userData),\n identify: (email: string, eventId?: string) =>\n enqueue('user.registered', { email, ...(eventId ? { eventId } : {}) }),\n setVariables: (variables: Record<string, unknown>) => {\n pendingVars = variables // full snapshot — latest wins\n if (varsTimer) clearTimeout(varsTimer)\n varsTimer = setTimeout(flushVars, VARS_DEBOUNCE_MS)\n },\n setAcquisition: (acq: Acquisition) => { acquisition = acq },\n // The session id AS IT STANDS NOW — `null` until the first event response mints one. The\n // checkout ctx reads this per attempt (flow/mount.tsx): a first-time visitor arrives with no\n // server-resolved session, so anything that snapshots this at mount charges with `sessionId:\n // null` forever, which is precisely how off-session upsells came to fail for new buyers while\n // working for returning ones.\n getSessionId: () => sessionId,\n }\n}\n","/**\n * The **mount contract** — `mount.tsx` is THE v2 convention for a funnel build.\n *\n * Every published funnel ships a `mount.tsx` that exports exactly two things\n * (the {@link MountModule} shape):\n *\n * - `pages: RuntimePage[]` — the code-split page list in file order (the\n * admin editor rewrites this array; the anchor\n * `export const pages: RuntimePage[] = [` must survive verbatim);\n * - `tree: (opts: MountOpts) => ReactNode` — built with\n * {@link createFunnelTree}, consumed identically by the server entry (SSR)\n * and the client entry (hydration) so both render the same tree.\n *\n * `createFunnelTree` packages the canonical provider stack so funnel builds\n * don't hand-roll it:\n *\n * AssetsProvider → FunnelProvider{config, offerings, locale, mode}\n * → FunnelView{pages, layout, initialKey, prefetch:'off'}\n *\n * (`prefetch: 'off'` because the renderer owns chunk warming via\n * `<link rel=\"modulepreload\">` over `useNavigation().nextCandidates`.)\n */\n\nimport {\n createContext,\n createElement,\n Fragment,\n useContext,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport { FunnelProvider } from '../state/namespaces'\nimport { FunnelView, EmbedBridge, type RuntimePage } from './flow'\nimport { syncFunnelUrl } from './url'\nimport type { FunnelDefinition } from './spine'\nimport type { OfferingInput } from '../commerce/money'\nimport { CheckoutResume, type CheckoutDriver, type CheckoutDriverContext } from '../commerce/checkout'\nimport type { RuntimeExperiment } from './experiments'\nimport type { MessageCatalog } from '../i18n/translation'\nimport { createPlatformTracker } from '../tracking/platformTracker'\n\n// ── Assets ───────────────────────────────────────────────\n\n/**\n * Funnel asset map: logical name (e.g. `logo.svg`) → content-addressed CDN URL,\n * injected by the renderer. Assets live in a SHARED content-addressed store\n * (`appfunnel/assets/<hash>`), so identical content is stored once across all\n * builds and funnels while staying immutable. Both SSR and client read the same\n * injected map → URLs match. Defaults to `{}` (no provider needed in tests).\n */\nexport const AssetsContext = createContext<Record<string, string>>({})\n\n/** Provides the funnel's asset map (mounted for you by {@link createFunnelTree}). */\nexport function AssetsProvider({\n assets,\n children,\n}: {\n assets?: Record<string, string>\n children: ReactNode\n}): ReactNode {\n return createElement(AssetsContext.Provider, { value: assets ?? {} }, children)\n}\n\n/** The whole asset map (logical name → CDN URL). `{}` outside a provider. */\nexport function useAssets(): Record<string, string> {\n return useContext(AssetsContext)\n}\n\n/** `useAsset('logo.svg')` — one asset's CDN URL, or `undefined` if not injected. */\nexport function useAsset(name: string): string | undefined {\n return useContext(AssetsContext)[name]\n}\n\n// ── Mount options ────────────────────────────────────────\n\n/** What the renderer injects when mounting a funnel build (SSR and hydration). */\nexport interface MountOpts {\n /** Platform-resolved offering prices (one currency per visitor). */\n offerings?: OfferingInput[]\n /** Explicit locale override (URL/param/geo); wins over the detected device locale. */\n locale?: string\n mode?: 'test' | 'live'\n /** Deep-link / reload entry page — a page KEY or its URL SLUG (honored only if its entry\n * guard passes). */\n initialKey?: string\n /**\n * The campaign's URL path (`/get-started`, or `/de/get-started` with a locale prefix).\n * Present → the SDK keeps the address bar on the live page (`<basePath>/<slug>`, bare basePath on\n * the start page) via `history.replaceState`, so a refresh/share/restore resumes where the visitor\n * is instead of restarting the funnel. Set by the renderer's LIVE route; absent on embedded\n * surfaces (editor preview, tests, bare mounts), which must not touch the host URL.\n */\n basePath?: string\n /** Asset map: logical name → content-addressed CDN URL. */\n assets?: Record<string, string>\n /** Per-locale message catalogs (`messages/<locale>.json`) — overrides the mount's baked-in ones. */\n messages?: MessageCatalog\n /** The SIGNED anonymous visitor id (bare id, verified server-side; phase-1 §1.2) — the\n * renderer mints/reads the `af_vid` cookie and injects the value here. Feeds\n * `context.identity.visitorId` (experiment bucketing fallback seed, session continuity). */\n visitorId?: string | null\n /** Known customer id (identified visitor) — the preferred bucketing seed. */\n customerId?: string | null\n /** Platform tracking config (phase-1 §1.4). Present → `createFunnelTree` wires the real\n * platform tracker (events POST to the shared ingestion endpoint). Absent (tests, bare mounts,\n * the editor's in-browser preview) → the console tracker. Serializable on purpose — it crosses\n * the sealed-build boundary as data. */\n tracking?: {\n apiBase: string\n /** Campaign DB id — the campaign ingestion route key. Absent on the build-preview surface,\n * which belongs to no campaign; `preview` carries that surface's routing instead. Exactly one\n * of the two is set. */\n campaignId?: string | null\n /** BUILD-PREVIEW ingestion (`/preview/<buildId>`, DESIGN-2026-07-28-preview-sessions §10 step 4).\n * Present → the tracker POSTs to the campaign-less preview endpoint with the build's preview\n * token. The SERVER derives ('preview','test') + buildId from that token; this only routes. */\n preview?: {\n /** The build being previewed — what the token is signed over. */\n buildId: string\n /** The HMAC preview token the renderer already required to serve this page. */\n token: string\n } | null\n /** WebFunnel DB id (NOT the funnel.ts config id). */\n funnelId: string\n /** FUNNEL-split assignment (phase-4) — the tracker emits its exposure event. */\n experiment?: { id: string; variant: string; version?: number }\n /** Campaign ROUTE that served this first request (campaign-routing pivot) — the tracker forwards\n * its id as `campaignRouteId` so ingestion stamps the session once at create. */\n route?: { id: string }\n }\n /**\n * BUILD-PREVIEW checkout config (`/preview/<buildId>`). Present → `createFunnelTree` wires the\n * REAL provider driver against the campaign-less preview endpoint, so an author can exercise an\n * actual test-mode checkout on an unpublished build instead of the mock.\n *\n * Deliberately separate from `tracking`: charging and ingesting are independent capabilities on\n * this surface (a deployment with no PREVIEW_TOKEN_SECRET can do neither, one with no\n * TRACKING_API_BASE can do neither — but the renderer decides each on its own facts). Set only by\n * the renderer's preview route; a live funnel never carries it. Serializable — it crosses the\n * sealed-build boundary as data.\n *\n * NOTE (2026-07-28): a preview no longer stays out of *ingestion* — it writes ('preview','test')\n * sessions via `tracking.preview`, which analytics excludes. It still stays out of every REPORT\n * and out of the integration/CAPI fan-out, which is what \"a preview must not pollute shared data\"\n * always actually meant.\n */\n previewCheckout?: {\n apiBase: string\n /** WebFunnel DB id (NOT the funnel.ts config id). */\n funnelId: string\n /** The build being previewed — what the token is signed over. */\n buildId: string\n /** The HMAC preview token the renderer already required to serve this page. */\n token: string\n }\n /** Server-hydrated namespace snapshot (phase-1 §1.1): flat namespaced keys\n * (`user.email`, `responses.goal`, …) from the visitor's latest session — the renderer\n * resolves it by visitorId so a returning visitor resumes with their answers. */\n sessionValues?: Record<string, string | number | boolean | null | string[]>\n /** RUNNING page-experiment records (phase-4): platform-owned wiring over the bundle's\n * `@variant` pages, injected at render time — no rebuild to start/stop/reweight. */\n experiments?: RuntimeExperiment[]\n /** The server-resolved session id backing `sessionValues` — seeds the tracker (fallback)\n * and `context.session.id`. */\n sessionId?: string | null\n /** Mount the cross-origin {@link EmbedBridge} — the Funnel Embed API (flow snapshot + goTo/goNext/\n * goPrevious + screenshot, over the versioned wire protocol). Set ONLY by preview surfaces (the live\n * editor + the sealed `/preview/<buildId>` build); a live prod funnel leaves it off so it can't be\n * driven / can't leak its flow state to an embedder. */\n embed?: boolean\n /** FLASH-FREE HYDRATION (SSR-isolation Option B): set by the CLIENT entry only. The initial\n * render is seeded with DEFAULTS (matching the build-time default-state prerender it hydrates\n * against), then `sessionValues` are applied post-hydration via `store.setMany` — a returning\n * visitor's saved answers pop into already-visible controls, no empty flash, no mismatch. Unset\n * on SSR (the server render must bake sessionValues into the markup, the original behavior). */\n deferSessionValues?: boolean\n /** SSR-isolation Option B, FIX 1: render `initialKey` VERBATIM without evaluating its entry guard.\n * Set by the build-time PRERENDER stage so a guarded page (paywall/upsell) prerenders its OWN\n * default-state markup (the renderer decides target-vs-bounce at serve time by evaluating the\n * guard against the visitor's session). Defaults to `deferSessionValues` (the client trusts the\n * server-resolved key); the prerender sets it true explicitly while leaving sessionValues baked. */\n trustInitialKey?: boolean\n}\n\n/**\n * The shape a funnel build's `mount.tsx` module must export — the named,\n * versioned contract between a build and the renderer/editor:\n * `import type { MountModule } from '@appfunnel-dev/sdk'`.\n */\nexport type MountModule = {\n pages: RuntimePage[]\n tree: (opts: MountOpts) => ReactNode\n}\n\n// ── createFunnelTree ─────────────────────────────────────\n\nexport interface CreateFunnelTreeInput {\n /** The funnel's spine (`defineFunnel(...)`). */\n config: FunnelDefinition\n /** Code-split pages in file order (the same array `mount.tsx` exports). */\n pages: RuntimePage[]\n /** Persistent chrome wrapped around the page area (mounted once). */\n layout?: ComponentType<{ children: ReactNode }>\n /**\n * The platform checkout driver factory — `mount.tsx` imports it from a driver\n * subpath entry (`import { stripeCheckoutDriver } from '@appfunnel-dev/sdk/driver-stripe'`)\n * and passes it here, so the provider choice stays in the SDK-owned authoring\n * contract while the ctx (apiBase/campaign/funnel/mode/identity) stays\n * renderer-injected. Called once per `tree()` mount, only when tracking config\n * is present — without it there is no API to charge against (preview, tests,\n * bare mounts) and the mock driver fallback stays in place. Test mode still\n * constructs the real driver: the wire contract carries `mode`, and the server\n * resolves the provider's TEST store from it (that's the quarantine, not the client).\n */\n checkoutDriver?: (ctx: CheckoutDriverContext) => CheckoutDriver\n /**\n * The funnel's message catalogs (`messages/<locale>.json`), keyed by locale. The generated\n * `mount.tsx` imports the catalogs and passes them here so `t()` resolves at render (SSR + client);\n * `MountOpts.messages` can still override per-request. Omit for funnels with no translations.\n */\n messages?: MessageCatalog\n}\n\n/**\n * Build a funnel's `tree(opts)` — the standard provider stack over your pages.\n *\n * ```tsx\n * export const pages: RuntimePage[] = [ ... ]\n * export const tree = createFunnelTree({ config, pages, layout: Layout })\n * ```\n */\nexport function createFunnelTree({\n config,\n pages,\n layout,\n checkoutDriver,\n messages,\n}: CreateFunnelTreeInput): (opts: MountOpts) => ReactNode {\n return function tree(opts: MountOpts): ReactNode {\n const mode = opts.mode ?? 'live'\n const embedEnabled = opts.embed ?? false\n // Narrowed once so the onNavigate closure below stays a plain string capture.\n const basePath = opts.basePath\n // The real platform tracker when the renderer passed tracking config (the factory itself\n // no-ops for SSR; since 2026-07-28 it no longer no-ops for test mode — `mode` rides the wire\n // and the server stamps it). One instance per tree() call: the entries\n // call tree() exactly once per mount (SSR render / hydration), so no memo hook is needed\n // and tree() stays callable outside React.\n const tracker = opts.tracking\n ? createPlatformTracker({\n ...opts.tracking,\n visitorId: opts.visitorId,\n sessionId: opts.sessionId,\n mode,\n })\n : undefined\n // The platform checkout driver when mount.tsx exported a factory AND the renderer passed\n // tracking config — the driver ctx derives from the SAME injected data the tracker uses,\n // so there is no second config channel. Unlike the tracker (whose factory no-ops on the\n // server), the driver IS constructed during SSR, deliberately: factories are\n // construction-safe closures over serializable ctx (provider SDKs load only in\n // effects/event handlers), and a driver's renderInline returns the same stable placeholder\n // on the server and the first client paint. Skipping construction server-side would SSR\n // the mock fallback's <button> into inline surfaces — a guaranteed hydration mismatch.\n // TWO surfaces can charge, and they carry different routing + credentials:\n // - `tracking` → live/campaign traffic, campaign-scoped endpoint;\n // - `previewCheckout` → an unpublished build being QA'd, campaign-less endpoint + the\n // build's preview token. Test mode only (the server re-asserts it).\n // Neither present (editor preview, tests, bare mounts) ⇒ no API to charge against, and the\n // mock driver fallback stays in place. A CAMPAIGN `tracking` config wins if both were somehow\n // supplied — a funnel serving real traffic must never be routed through the preview endpoint.\n // Keyed on `campaignId` specifically, not on `tracking` being present at all: since 2026-07-28\n // the preview surface ALSO passes `tracking` (campaign-less, `tracking.preview`), and routing\n // its checkout through the campaign branch would produce an empty campaign id in the URL.\n // THE SESSION IS READ PER ATTEMPT, not snapshotted at mount. `opts.sessionId` is only what the\n // SERVER could resolve from the visitor cookie, which is `null` for anyone arriving for the\n // first time; the real id is minted by the first event response and lives on the tracker. A\n // getter is what makes the two agree: an upsell that fires minutes after the paywall reads the\n // session that exists BY THEN, instead of the nothing that existed at mount.\n //\n // This is not cosmetic. `runStripeUpsell` refuses an upsell with no `sessionId` (\"An upsell\n // needs the funnel session of a previous purchase\") and the error is retryable, so the SDK\n // opened its recovery sheet and asked for a card — turning the one-tap off-session charge into\n // a card form, for FIRST-TIME BUYERS ONLY. Returning visitors carried a server-resolved id and\n // worked, which is exactly why it survived testing.\n const liveSessionId = (): string | null | undefined => tracker?.getSessionId?.() ?? opts.sessionId\n const checkoutCtx: CheckoutDriverContext | undefined = opts.tracking?.campaignId\n ? {\n apiBase: opts.tracking.apiBase,\n campaignId: opts.tracking.campaignId,\n funnelId: opts.tracking.funnelId,\n mode,\n get sessionId() {\n return liveSessionId()\n },\n visitorId: opts.visitorId,\n }\n : opts.previewCheckout\n ? {\n apiBase: opts.previewCheckout.apiBase,\n funnelId: opts.previewCheckout.funnelId,\n preview: { buildId: opts.previewCheckout.buildId, token: opts.previewCheckout.token },\n mode,\n get sessionId() {\n return liveSessionId()\n },\n visitorId: opts.visitorId,\n }\n : undefined\n const checkout = checkoutDriver && checkoutCtx ? checkoutDriver(checkoutCtx) : undefined\n // The redirect-return resume leg (hosted-checkout success URLs carry\n // `?af_checkout={correlationId}`) rides the persistent layout slot: it needs\n // tracker + navigation, which only exist INSIDE FunnelView's nav context, and\n // the layout is the one mount-once spot there. SSR-safe (renders null;\n // effect-only) and inert without the param.\n const layoutWithResume: ComponentType<{ children: ReactNode }> = function LayoutWithCheckoutResume({\n children,\n }: {\n children: ReactNode\n }): ReactNode {\n return createElement(\n Fragment,\n null,\n createElement(CheckoutResume, null),\n embedEnabled ? createElement(EmbedBridge, null) : null,\n layout ? createElement(layout, null, children) : children,\n )\n }\n return createElement(AssetsProvider, {\n assets: opts.assets,\n children: createElement(FunnelProvider, {\n config,\n offerings: opts.offerings ?? [],\n messages: opts.messages ?? messages,\n locale: opts.locale,\n tracker,\n checkout,\n sessionValues: opts.sessionValues,\n deferSessionValues: opts.deferSessionValues,\n experiments: opts.experiments,\n context: {\n mode,\n visitorId: opts.visitorId ?? null,\n customerId: opts.customerId ?? null,\n sessionId: opts.sessionId ?? null,\n },\n children: createElement(FunnelView, {\n pages,\n layout: layoutWithResume,\n initialKey: opts.initialKey,\n // FLASH-FREE HYDRATION (Option B, FIX 1): trust the SERVER-resolved initialKey instead of\n // re-running the entry guard. The CLIENT sets this via deferSessionValues (its deferred\n // snapshot is empty, so re-running the guard would bounce a qualified deep-link and\n // mismatch the markup). The build-time PRERENDER sets `trustInitialKey` explicitly so a\n // guarded page prerenders its OWN default-state markup (the renderer picks target-vs-bounce\n // at serve time). Live SSR fallback leaves both off → the guard runs against real values.\n trustInitialKey: opts.trustInitialKey ?? opts.deferSessionValues,\n prefetch: 'off', // renderer background-modulepreloads all chunks\n // URL PATH SYNC: keep the address bar on the live page whenever the renderer told us the\n // campaign's path. Without it the URL freezes at the entry and a refresh restarts the\n // funnel. Effect-only (never runs during SSR) and absent on embedded surfaces.\n onNavigate: basePath\n ? (_key, slug, isStart) => syncFunnelUrl(basePath, slug, isStart)\n : undefined,\n }),\n }),\n })\n }\n}\n","import {\n createElement,\n type ButtonHTMLAttributes,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from 'react'\nimport { useField } from '../state/namespaces'\nimport { useNavigation } from './flow'\n\n/**\n * The two-tier component model (doc 07 §\"magic ≠ black box\"): the magic lives in\n * headless hooks (`useField`, `useNavigation`); these styleable components are\n * thin, restyle-able wrappers over them. Every `<Choice>` reduces to `useField`\n * + your own JSX — eject any time. They pass `className`/DOM props straight\n * through and never dictate a pixel beyond the minimum.\n */\n\nexport interface ChoiceOption {\n value: string\n label?: ReactNode\n}\n\nexport interface ChoiceProps {\n /** Writable namespaced path to bind, e.g. `responses.goal`. */\n bind: string\n /** The selectable options (a bare string is shorthand for `{value, label}`). */\n options: (string | ChoiceOption)[]\n /** Class applied to each option button. */\n className?: string\n /** Class applied to the selected option button (in addition to `className`). */\n selectedClassName?: string\n /** Advance navigation after a choice (handy for single-tap question pages). */\n advanceOnSelect?: boolean\n}\n\n/**\n * Single-select bound to a writable field. `<Choice bind=\"responses.goal\"\n * options={[\"lose\", \"gain\"]} />` — selecting persists + (later) tracks; pass\n * `advanceOnSelect` to move to the next page on tap.\n */\nexport function Choice({\n bind,\n options,\n className,\n selectedClassName,\n advanceOnSelect,\n}: ChoiceProps): ReactNode {\n const { value, set } = useField(bind)\n const nav = useNavigation()\n\n return options.map((opt) => {\n const o: ChoiceOption = typeof opt === 'string' ? { value: opt } : opt\n const selected = value === o.value\n return createElement(\n 'button',\n {\n key: o.value,\n type: 'button',\n 'aria-pressed': selected,\n 'data-selected': selected || undefined,\n className: selected && selectedClassName ? `${className ?? ''} ${selectedClassName}`.trim() : className,\n onClick: () => {\n set(o.value)\n if (advanceOnSelect) nav.next()\n },\n },\n o.label ?? o.value,\n )\n })\n}\n\nexport interface NextProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n children?: ReactNode\n}\n\n/** A button that advances the funnel (the current page's `next`, else linear). */\nexport function Next({ children, onClick, ...rest }: NextProps): ReactNode {\n const { next } = useNavigation()\n return createElement(\n 'button',\n {\n type: 'button',\n ...rest,\n onClick: (e: ReactMouseEvent<HTMLButtonElement>) => {\n onClick?.(e)\n if (!e.defaultPrevented) next()\n },\n },\n children ?? 'Continue',\n )\n}\n\nexport interface BackProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n children?: ReactNode\n}\n\n/** A button that returns to the previous page; hidden when there's no history. */\nexport function Back({ children, onClick, ...rest }: BackProps): ReactNode {\n const { back, canGoBack } = useNavigation()\n if (!canGoBack) return null\n return createElement(\n 'button',\n {\n type: 'button',\n ...rest,\n onClick: (e: ReactMouseEvent<HTMLButtonElement>) => {\n onClick?.(e)\n if (!e.defaultPrevented) back()\n },\n },\n children ?? 'Back',\n )\n}\n","/**\n * WHICH STORE — the only question the funnel's last screen can actually answer.\n *\n * The funnel's terminal page has to send the visitor to the App Store or Google Play, and it has\n * to pick without asking them. That is what this module does, and deliberately all it does.\n *\n * WHY THERE IS NO PARAMETER MACHINERY HERE. An earlier version of this module built a URL out of\n * the funnel's identity, answers and ad click-ids and appended it to the store link. That is\n * theatre: the App Store DISCARDS every query parameter, so on iOS — 85.7% of this product's\n * traffic — nothing appended ever reaches the app. Google Play carries exactly one (`referrer`).\n * A component whose headline feature works on neither store as advertised is worse than one that\n * doesn't claim it, so the claim is gone.\n *\n * Carrying identity across an install is a real problem with real solutions — a universal link\n * (only if the app is already installed), a deferred-deeplink vendor (probabilistic fingerprint\n * matching), or joining on identity inside the app. None of them are a query string on a store\n * URL, and none of them belong in a button. If the destination is a tracker link that does its own\n * routing, pass it as `fallback` and it will be used for everyone.\n */\n\nimport type { FunnelContext } from '../state/context'\n\n/**\n * The store platform, collapsed to what a download button can act on. `other` is desktop and\n * anything unrecognised — a real destination in its own right (a QR page, the marketing site),\n * not an error state.\n */\nexport type AppPlatform = 'ios' | 'android' | 'other'\n\n/**\n * The platform for a funnel context. Reads `context.os.name` rather than sniffing the user-agent\n * again: the SDK has exactly one detector ({@link ../state/context}.osFromUserAgent) and a second\n * one drifting alongside it is how a funnel ends up sending iPhones to Google Play.\n *\n * That detector was wrong until 2026-07-31 — every iOS user-agent contains \"like Mac OS X\", so the\n * mac test matched first and reported every iPhone as macOS. Replaying all 144,938 stored sessions\n * put 85.7% of them in the wrong bucket. This function is only as good as that fix, which is why\n * the fix has its own test file pinned against real user-agents.\n */\nexport function platformFor(context: FunnelContext): AppPlatform {\n\tconst os = context.os?.name\n\tif (os === 'iOS') return 'ios'\n\tif (os === 'Android') return 'android'\n\treturn 'other'\n}\n\n/** Where each platform goes. Every field optional — give the stores you actually have. */\nexport interface AppStores {\n\t/** App Store URL (or a universal link / tracker link, if that is your iOS destination). */\n\tios?: string\n\t/** Google Play URL (or a universal link / tracker link). */\n\tandroid?: string\n\t/**\n\t * Desktop and anything unrecognised — a QR page, the marketing site, or a single tracker link\n\t * that does its own platform routing.\n\t */\n\tfallback?: string\n}\n\n/** `scheme:` at the head — `https:`, `myapp:`, anything. */\nconst HAS_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/\n\n/**\n * Normalise a destination: a bare host/path gets `https://`. Already-schemed URLs,\n * protocol-relative (`//cdn…`), absolute paths (`/thanks`) and bare `?`/`#` are returned as given.\n */\nexport function withScheme(url: string): string {\n\tconst t = url.trim()\n\tif (!t) return ''\n\tif (HAS_SCHEME.test(t)) return t\n\tif (t.startsWith('//') || t.startsWith('/') || t.startsWith('?') || t.startsWith('#')) return t\n\treturn `https://${t}`\n}\n\n/**\n * Pick the store URL for a platform, specific → generic.\n *\n * The `other` chain ends in a store URL rather than nothing on purpose: a desktop visitor landing\n * on the App Store's web listing is a mildly wrong destination, whereas a dead button on the page\n * they just paid on is a refund.\n */\nexport function resolveStore(stores: AppStores, platform: AppPlatform): string {\n\tconst ordered =\n\t\tplatform === 'ios'\n\t\t\t? [stores.ios, stores.fallback, stores.android]\n\t\t\t: platform === 'android'\n\t\t\t\t? [stores.android, stores.fallback, stores.ios]\n\t\t\t\t: [stores.fallback, stores.ios, stores.android]\n\treturn withScheme(ordered.find((v) => v && v.trim()) ?? '')\n}\n","/**\n * The React half of the app download — {@link useAppDownload} (headless) and\n * {@link DownloadButton} (the styleable wrapper over it), following the same two-tier model as\n * `useField`/`<Choice>`: the decision lives in the hook, the component is a thin `<a>` that passes\n * every DOM prop straight through and dictates no pixel.\n *\n * TEST MODE DOES NOT NAVIGATE. Under `mode: 'test'` — the editor preview and `?af_mode=test` QA —\n * the store URL is logged to the console and the click still tracks, but the browser stays put. A\n * preview that navigated to the App Store on the finish page would tear the editor's iframe out\n * from under the author and end every automated preview walk one page early.\n *\n * NO STORE-BADGE COMPONENT, deliberately. The SDK cannot style a badge that matches an arbitrary\n * template's design tokens, and every ready-made component added here is a permanent public-API\n * commitment (see ../elements/index.ts). Both badges side by side is this component twice, with\n * the template's own markup as children.\n */\n\nimport {\n\tcreateElement,\n\tuseCallback,\n\tuseRef,\n\ttype AnchorHTMLAttributes,\n\ttype MouseEvent as ReactMouseEvent,\n\ttype ReactNode,\n} from 'react'\nimport { useFunnel } from '../state/namespaces'\nimport { platformFor, resolveStore, type AppPlatform, type AppStores } from './store'\n\n/** Where the store page opens. Same values as the HTML attribute, and passed through as one. */\nexport type DownloadTarget = '_self' | '_blank'\n\n/**\n * `usePlatform()` — `'ios' | 'android' | 'other'` for the current visitor. The store decision on\n * its own, so copy and artwork can agree with where the button goes\n * (`platform === 'android' ? 'Get it on Google Play' : 'Download on the App Store'`).\n */\nexport function usePlatform(): AppPlatform {\n\treturn platformFor(useFunnel().context)\n}\n\nexport interface UseAppDownloadResult {\n\t/** The visitor's platform — which of `ios`/`android`/`fallback` was chosen. */\n\tplatform: AppPlatform\n\t/** The store URL for that platform, `https://`-normalised. Empty if nothing was configured. */\n\turl: string\n\t/** Track the click and go. Returns the URL; in test mode it logs instead of navigating. */\n\topen: (target?: DownloadTarget) => string\n}\n\n/** What {@link useAppDownload} accepts on top of the store URLs. */\nexport interface DownloadTracking {\n\t/**\n\t * Analytics event emitted on click, defaulting to `download.click` with `{ url, platform }`.\n\t * Pass a string to rename it, or `false` to emit nothing.\n\t */\n\tevent?: string | false\n\t/** Where the store opens. Default `_self`. */\n\ttarget?: DownloadTarget\n}\n\n/**\n * The headless download — everything {@link DownloadButton} does, without the markup. Reach for it\n * when the handoff isn't a link: a redirect after a countdown, a QR code's payload, a button that\n * has to do other things first.\n *\n * @example\n * const app = useAppDownload({\n * ios: 'https://apps.apple.com/app/id1234567890',\n * android: 'https://play.google.com/store/apps/details?id=com.acme.app',\n * })\n * return <button onClick={() => app.open()}>Download the app</button>\n */\nexport function useAppDownload(\n\tstores: AppStores & DownloadTracking\n): UseAppDownloadResult {\n\tconst funnel = useFunnel()\n\t// Props arrive as a fresh object literal every render; a ref keeps `open` stable without\n\t// asking callers to memoise.\n\tconst latest = useRef(stores)\n\tlatest.current = stores\n\n\tconst platform = platformFor(funnel.context)\n\tconst url = resolveStore(stores, platform)\n\n\tconst open = useCallback(\n\t\t(target?: DownloadTarget) => {\n\t\t\tconst s = latest.current\n\t\t\tconst p = platformFor(funnel.context)\n\t\t\tconst to = resolveStore(s, p)\n\t\t\tif (!to) return ''\n\t\t\tif (s.event !== false) funnel.track(s.event ?? 'download.click', { url: to, platform: p })\n\t\t\tif (funnel.context.system.mode === 'test') {\n\t\t\t\tconsole.info('[appfunnel] app download (test mode — not navigating):', to)\n\t\t\t\treturn to\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tif ((target ?? s.target ?? '_self') === '_blank')\n\t\t\t\t\twindow.open(to, '_blank', 'noopener,noreferrer')\n\t\t\t\telse window.location.href = to\n\t\t\t} catch {\n\t\t\t\t// Sandboxed frames can refuse top-level navigation. The click was still tracked;\n\t\t\t\t// swallowing keeps a blocked handoff from crashing the finish page.\n\t\t\t}\n\t\t\treturn to\n\t\t},\n\t\t[funnel]\n\t)\n\n\treturn { platform, url, open }\n}\n\nexport interface DownloadButtonProps\n\textends AppStores,\n\t\tDownloadTracking,\n\t\tOmit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'target'> {\n\tchildren?: ReactNode\n}\n\n/**\n * A link to the app store, picked from the visitor's device.\n *\n * iPhones and iPads get `ios`, Android phones get `android`, everyone else gets `fallback` — so\n * the finish page is one button rather than a row of badges the visitor has to choose between.\n * Renders a plain `<a href=\"{store url}\">`: real link semantics, keyboard-focusable, and styleable\n * entirely through `className`/`style` like any anchor.\n *\n * Calling `preventDefault()` in your own `onClick` cancels the navigation, matching `<Next>`/`<Back>`.\n *\n * @example\n * <DownloadButton\n * ios=\"https://apps.apple.com/app/id1234567890\"\n * android=\"https://play.google.com/store/apps/details?id=com.acme.app\"\n * fallback=\"https://acme.com/get-the-app\"\n * className=\"btn-primary\"\n * >\n * Download the app\n * </DownloadButton>\n *\n * @example\n * // Both badges side by side — the same component twice, wrapping the template's own markup.\n * <DownloadButton ios={appStoreUrl} fallback={appStoreUrl} target=\"_blank\"><AppStoreBadge /></DownloadButton>\n * <DownloadButton android={playUrl} fallback={playUrl} target=\"_blank\"><GooglePlayBadge /></DownloadButton>\n */\nexport function DownloadButton({\n\tios,\n\tandroid,\n\tfallback,\n\tevent,\n\ttarget = '_self',\n\tchildren,\n\tonClick,\n\t...rest\n}: DownloadButtonProps): ReactNode {\n\tconst app = useAppDownload({ ios, android, fallback, event, target })\n\treturn createElement(\n\t\t'a',\n\t\t{\n\t\t\t...rest,\n\t\t\thref: app.url || undefined,\n\t\t\ttarget,\n\t\t\t// A `_blank` link without this leaves the opened page a handle on the funnel window.\n\t\t\trel: rest.rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined),\n\t\t\tonClick: (e: ReactMouseEvent<HTMLAnchorElement>) => {\n\t\t\t\tonClick?.(e)\n\t\t\t\tif (e.defaultPrevented) return\n\t\t\t\t// The anchor already points at the right store, so a plain navigation would work.\n\t\t\t\t// Going through `open` instead is what makes the click trackable and keeps the\n\t\t\t\t// editor preview from walking itself out of the funnel.\n\t\t\t\te.preventDefault()\n\t\t\t\tapp.open(target)\n\t\t\t},\n\t\t},\n\t\tchildren ?? 'Download the app'\n\t)\n}\n","/**\n * The **event catalog** — the single source of truth for *which* events exist and\n * how each is routed (analytics vs. integrations vs. runtime-only). Pairs with the\n * typed payloads in {@link ./tracking}.`FunnelEventDataMap`.\n *\n * This replaces the duplication the monorepo flags (`shared/events/catalog.ts` is\n * \"documentation, not imported\" — the taxonomy is re-declared in\n * `admin/lib/funnel/tracking.ts`, each `admin/lib/integrations/*` EVENT_MAP, and\n * `api/src/integrations/constants`). In v2 the **SDK owns the taxonomy** (it emits\n * it), so the bus, the pixel loaders, and the platform all import *this*.\n */\n\nexport type EventSource = 'frontend' | 'backend'\n\nexport interface EventMeta {\n /** Where the event originates. `backend` = the server emits it (e.g. a settled charge). */\n sources: EventSource[]\n /** Persisted to analytics. */\n tracking: boolean\n /** Forwarded to ad/integration pixels (Meta/GTM/TikTok/…). */\n integrations: boolean\n /** Runtime-only JS subscription — not persisted, not sent to integrations. */\n runtime?: boolean\n}\n\n/**\n * Every event the system knows, with its routing metadata. Keys are the taxonomy\n * names (a superset of `FunnelEventDataMap`: it also lists the **backend-originated**\n * monetary events the platform pushes to the client bus for pixel firing).\n */\nexport const EVENT_CATALOG = {\n // ── Runtime / navigation (JS subscriptions; not persisted) ──\n 'funnel.start': { sources: ['frontend'], tracking: false, integrations: false, runtime: true },\n 'experiment.exposure': { sources: ['frontend'], tracking: true, integrations: false },\n\n // ── Frontend, tracked; some also go to pixels ──\n 'page.view': { sources: ['frontend'], tracking: true, integrations: true },\n 'page.exit': { sources: ['frontend'], tracking: true, integrations: false },\n 'user.registered': { sources: ['frontend'], tracking: true, integrations: true },\n 'checkout.start': { sources: ['frontend'], tracking: true, integrations: true },\n 'checkout.payment_added': { sources: ['frontend'], tracking: true, integrations: true },\n 'checkout.failed': { sources: ['frontend'], tracking: true, integrations: false },\n // A charge the platform refused because the buyer already owns the product. `integrations:\n // false` is the load-bearing flag: the flow advances as if the purchase succeeded, but no\n // money moved on THIS attempt, so nothing may reach an ad pixel — the original purchase\n // already fired its Purchase/Subscribe with the server-minted dedup id.\n 'checkout.already_purchased': { sources: ['frontend'], tracking: true, integrations: false },\n\n // ── Backend-originated money events (server mints the eventId; client fires the pixel) ──\n 'purchase.complete': { sources: ['backend'], tracking: true, integrations: true },\n 'customer.first_purchase': { sources: ['backend'], tracking: true, integrations: true },\n 'subscription.created': { sources: ['backend'], tracking: true, integrations: true },\n // A trial START — free or paid. Fires INSTEAD OF purchase.complete/subscription.created for a\n // trial settlement: one event per settlement, and a taxonomy that can distinguish a trial from\n // an outright sale. Value is the money that moved (0 free / fee paid), never the sticker price\n // of the subscription behind it. Ad networks map it back onto `Purchase` with every other\n // acquisition; the distinction is kept for analytics/GTM/customer.io.\n 'subscription.trial_started': { sources: ['backend'], tracking: true, integrations: true },\n // A recurring charge and the FIRST charge after a trial are economically different events, and\n // neither is an initial purchase. Keeping them apart is load-bearing for ad networks: collapsing\n // them onto the same name as `purchase.complete` double-counts one customer in the ad account's\n // Purchase column, trains the bidder on money no click caused, and inflates reported ROAS.\n 'subscription.trial_converted': { sources: ['backend'], tracking: true, integrations: true },\n 'subscription.renewal': { sources: ['backend'], tracking: true, integrations: true },\n} as const satisfies Record<string, EventMeta>\n\n/** A catalog event name. */\nexport type CatalogEvent = keyof typeof EVENT_CATALOG\n\n/** True if the event is persisted to analytics. */\nexport function isTrackableEvent(name: string): boolean {\n return EVENT_CATALOG[name as CatalogEvent]?.tracking ?? false\n}\n\n/** True if the event is forwarded to ad/integration pixels. */\nexport function isIntegrationEvent(name: string): boolean {\n return EVENT_CATALOG[name as CatalogEvent]?.integrations ?? false\n}\n\n/** The events that feed integrations — the set a pixel loader subscribes to. */\nexport function integrationEvents(): CatalogEvent[] {\n return (Object.keys(EVENT_CATALOG) as CatalogEvent[]).filter((e) => EVENT_CATALOG[e].integrations)\n}\n","/**\n * `<Script>` — the **declared third-party embed** primitive (phase-2 §2.3d).\n *\n * The controlled escape hatch for runtime widgets/embeds that genuinely need\n * client code — Intercom, Trustpilot, a calendar embed, an operator pixel. Unlike\n * arbitrary `<script>` injection, it's **declared** (the funnel author writes\n * `<Script src=…/>`), **deduped** (injected at most once per id/src, even across\n * page navigation in the SPA — a chat widget must not reload on every page), and\n * **CSP-friendly** (accepts a `nonce`).\n *\n * CSP enforcement itself is the **renderer's** job: it scans the funnel's declared\n * `<Script src>`s (build-time) into the `script-src` allowlist and supplies the\n * `nonce`. This component just injects the tag at the right time.\n *\n * For *analytics* pixels, prefer subscribing to the `window.appfunnel` bus\n * ({@link ../tracking/bus}) — `<Script>` is for non-analytics third-party widgets\n * (and the pixel loader snippets, which then subscribe to the bus).\n */\n\nimport { useEffect } from 'react'\n\n/** Module-level registry so a given script injects once for the app's lifetime. */\nconst injected = new Set<string>()\n\nexport interface ScriptProps {\n /** The third-party script URL. */\n src: string\n /**\n * When to inject. `afterInteractive` (default) injects on mount (after hydration);\n * `lazyOnload` waits for idle/`load` so it never competes with the funnel's own JS.\n */\n strategy?: 'afterInteractive' | 'lazyOnload'\n /** Dedup key (defaults to `src`) — the script injects at most once per id. */\n id?: string\n /** Defaults to `true`. */\n async?: boolean\n defer?: boolean\n /** CSP nonce — supplied by the renderer. */\n nonce?: string\n /** Extra attributes (e.g. `data-*`). */\n attributes?: Record<string, string>\n onLoad?: () => void\n onError?: () => void\n}\n\n/**\n * Injects a third-party script once. Renders nothing. Persists across navigation\n * (the tag is never removed on unmount — third-party widgets should stay loaded).\n */\nexport function Script({\n src,\n strategy = 'afterInteractive',\n id,\n async = true,\n defer,\n nonce,\n attributes,\n onLoad,\n onError,\n}: ScriptProps): null {\n useEffect(() => {\n if (typeof document === 'undefined') return\n const key = id ?? src\n if (injected.has(key)) {\n onLoad?.() // already present from an earlier mount\n return\n }\n\n const inject = () => {\n if (injected.has(key)) return\n injected.add(key)\n const el = document.createElement('script')\n el.src = src\n if (id) el.id = id\n if (async) el.async = true\n if (defer) el.defer = true\n if (nonce) el.nonce = nonce\n if (attributes) for (const [k, v] of Object.entries(attributes)) el.setAttribute(k, v)\n el.addEventListener('load', () => onLoad?.())\n el.addEventListener('error', () => {\n injected.delete(key) // allow a later retry\n onError?.()\n })\n document.head.appendChild(el)\n }\n\n if (strategy === 'lazyOnload') {\n const ric = (window as unknown as { requestIdleCallback?: (cb: () => void) => number })\n .requestIdleCallback\n if (ric) {\n ric(inject)\n } else {\n const t = setTimeout(inject, 0)\n return () => clearTimeout(t)\n }\n return\n }\n inject()\n // src/id identify the script; injection is idempotent via the registry.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [src, id])\n\n return null\n}\n\n/** Internal: clear the injected-script registry (tests only). */\nexport function __resetScriptRegistry(): void {\n injected.clear()\n}\n"]}