Skip to Content
ReferenceFull API indexFlow & pages

Flow & pages

Every flow export of @appfunnel-dev/sdk, generated from the SDK source. Signatures and descriptions are exact; for how these fit together, see the guides.

useAsset

hook · flow/mount.tsx

useAsset(name: string): string | undefined

useAsset('logo.svg') — one asset’s CDN URL, or undefined if not injected.

useAssets

hook · flow/mount.tsx

useAssets(): Record<string, string>

The whole asset map (logical name → CDN URL). {} outside a provider.

useExperiment

hook · flow/flow.tsx

useExperiment(id: string): string | undefined

const variant = useExperiment('welcome-headline') — the variant key this visitor is assigned in an experiment (undefined if not in one). Page-variant swapping is automatic; this is for branching within a page or custom logging.

useGroupProgress

hook · flow/flow.tsx

useGroupProgress(): GroupProgress

useGroupProgress() — the visitor-routed, branch-aware grouped-progress structure for a grouped <ProgressBar>: ordered PageGroup segments (from meta.group), the activeIndex of the current group (-1 when the current page is ungrouped), and the active group’s within-group activeFraction (0–1). Counts only the pages THIS visitor will actually visit given their answers (see groupProgress). A funnel with no group keys yields { groups: [], activeIndex: -1, activeFraction: 0 }. Must be used inside <FunnelView>.

Each segment’s key is the slug identifier from PageMeta.group, NOT a display string — resolve it to a label in your funnel code, e.g. const GROUP_LABELS = { profile: 'My Profile' }; GROUP_LABELS[group.key] or t(groups.${group.key}). This keeps grouping stable across copy and locale changes.

useNavigation

hook · flow/flow.tsx

useNavigation(): NavigationState

const { next, back, go, page } = useNavigation().

usePage

hook · flow/flow.tsx

usePage(): PageContext

usePage() — the current page’s read-only context, reactive to navigation.

AssetsProvider

component · flow/mount.tsx

AssetsProvider({ assets, children, }: { assets?: Record<string, string>; children: ReactNode; }): ReactNode

Provides the funnel’s asset map (mounted for you by createFunnelTree).

Back

component · flow/components.tsx

Back({ children, onClick, ...rest }: BackProps): ReactNode

A button that returns to the previous page; hidden when there’s no history.

Choice

component · flow/components.tsx

Choice({ bind, options, className, selectedClassName, advanceOnSelect, }: ChoiceProps): ReactNode

Single-select bound to a writable field. <Choice bind="responses.goal" options={["lose", "gain"]} /> — selecting persists + (later) tracks; pass advanceOnSelect to move to the next page on tap.

FunnelView

component · flow/flow.tsx

FunnelView({ pages, initialKey, trustInitialKey, layout, fallback, prefetch: prefetchMode, onNavigate, }: { pages: RuntimePage[]; initialKey?: string; trustInitialKey?: boolean; layout?: ComponentType<{ children: ReactNode; }>; fallback?: ReactNode; prefetch?: "auto" | "off"; onNavigate?: (key: string, slug: string, isStart: boolean) => void; }): ReactNode

Renders the funnel: shows the current page and drives navigation. next() resolves the current page’s next predicate against the live snapshot (writable namespaces + read-only context), falling back to the linear next page in file order (see nextPage).

Auto-emits funnel.start on mount and page.view on each page change, and keeps context.page in sync so predicates and usePage() see the live page.

Pass a layout (a { children } component) for persistent chrome — a header, progress bar, background — rendered inside the nav context but outside the page swap, so it mounts once and never remounts on navigation (and can read usePage/useNavigation).

Must live inside <FunnelProvider>.

Next

component · flow/components.tsx

Next({ children, onClick, ...rest }: NextProps): ReactNode

A button that advances the funnel (the current page’s next, else linear).

assignVariant

function · flow/experiments.ts

assignVariant(experimentId: string, seed: string, weights: Record<string, number>): string

Deterministically assign a visitor (seed) to a variant of one experiment. The hash is namespaced by experimentId so two experiments on the same traffic don’t correlate (orthogonal layering).

bucketingSeed

function · flow/experiments.ts

bucketingSeed(context: FunnelContext): string | null

The stable bucketing seed: the signed visitorId, else the known customerId. Never the session id or fingerprint (landmine #4). null when neither is known — the caller then serves control instead of bucketing on nothing.

visitorId-FIRST (not customerId-first) is deliberate: the customerId injected for an anonymous visitor is minted server-side AFTER the first tracked event, so it is absent on visit 1 and present on visit 2 — seeding on it would flip a returning visitor’s arm and count them in two arms of the same version. The signed af_vid cookie is the durable identity that’s stable from the first pageload, so it seeds; customerId is only the fallback for cookieless SDK embeds. This matches the server-side FUNNEL split, which already seeds on the verified visitorId (resolve.ts). (Cross-device stable bucketing for IDENTIFIED customers is P3’s IdentitySDK job, not this anonymous path.)

createFunnelTree

function · flow/mount.tsx

createFunnelTree({ config, pages, layout, checkoutDriver, messages, }: CreateFunnelTreeInput): (opts: MountOpts) => ReactNode

Build a funnel’s tree(opts) — the standard provider stack over your pages.

export const pages: RuntimePage[] = [ ... ] export const tree = createFunnelTree({ config, pages, layout: Layout })

defineFunnel

function · flow/spine.ts

defineFunnel(def: FunnelDefinition): FunnelDefinition

Identity helper for a funnel definition.

definePage

function · flow/spine.ts

definePage<P extends object = Record<string, never>>(component: ComponentType<P>): PageComponent<P>

Identity helper for a page component. The page is plain React that reaches the runtime through hooks (useResponse, useNavigation, …) — there is no sdk prop. export default definePage(function Welcome() { … }).

Image preloading is AUTOMATIC and requires no per-page declaration: the SDK pre-mounts the likely-next pages HIDDEN (React <Activity mode="hidden">) so the browser fetches their above-the-fold imagery before the visitor arrives — no pop-in — while their effects stay deferred until the page is actually shown. See FunnelView.

entryGuard

function · flow/spine.ts

entryGuard(meta?: PageMeta): Gate | undefined

The effective entry guard for a page: an explicit meta.guard if the author set one, otherwise the page-type default (e.g. paywall/upsell need email). The explicit guard fully overrides the default — it’s not combined. Returns undefined when neither applies.

evaluateCondition

function · flow/spine.ts

evaluateCondition(cond: Condition, s: FunnelSnapshot): boolean

Evaluate a declarative Condition against a snapshot.

evaluateGate

function · flow/spine.ts

evaluateGate(gate: Gate, s: FunnelSnapshot): boolean

Evaluate either gate kind to a boolean.

expectedPathLength

function · flow/spine.ts

expectedPathLength(pages: FlowPage[], startKey: string, s: FunnelSnapshot): number

The number of pages on the path from startKey to a finish (or the end of the flow) given the current state — the denominator for progress. It walks the actual routing (nextPage) rather than counting all files, so a branched funnel reaches 100% on whichever path the visitor’s answers select. Re-traced per navigation; a visited set guards against routing cycles.

fnv1a

function · flow/experiments.ts

fnv1a(input: string): number

FNV-1a 32-bit hash → unsigned int. Deterministic and isomorphic (no crypto, runs the same in the browser, a worker, and a build). The same math v0 used, kept because it’s sound — only the seed changes (identity, not session).

groupProgress

function · flow/spine.ts

groupProgress(pages: FlowPage[], startKey: string, currentKey: string, s: FunnelSnapshot): GroupProgress

Build the grouped-progress structure for the CURRENT visitor by walking the actual ROUTED path from startKey (mirrors expectedPathLength: first-match routes against the live snapshot, a visited-guard against cycles, stopping at a finish) — NOT the static file order. A visitor on the muscle branch therefore gets the muscle-variant pages counted in their groups, not the lose-variant ones.

Rules (documented decisions):

  • Distinct by key, first-appearance order. Groups key on the slug; a key is listed once, positioned by where it first appears. A later re-appearance of the same key folds back into that one group (its pages add to the same count) — it does NOT open a second segment.
  • Ungrouped pages don’t break contiguity. A page with no group interleaved inside a group’s span belongs to no segment (counted in nothing), but neither does it split the surrounding group — because grouping is by key, not by runs.
  • Completion by position. completedCount counts a group’s pages strictly before the current page’s index in the routed path; the current page is the in-progress step (credited via activeFraction, not completedCount).
  • Current page ungrouped ⇒ activeIndex = -1, activeFraction = 0 (other groups still report their completion relative to the cursor).
  • Loops / back-navigation. Position is the current page’s…

hashToUnit

function · flow/experiments.ts

hashToUnit(seed: string): number

Map a seed string to a stable float in [0, 1).

isVariantKey

function · flow/experiments.ts

isVariantKey(key: string): boolean

True if a page key is an off-flow variant (welcome@b), by the source-side @ marker.

nextPage

function · flow/spine.ts

nextPage(pages: FlowPage[], currentKey: string, s: FunnelSnapshot): string | undefined

Decide the next page from currentKey:

  1. the current page’s next routes (resolveRoute), if one matches, else
  2. the next page in file order (the default linear flow), else
  3. undefined (end of funnel / current key not found).

outgoingKeys

function · flow/spine.ts

outgoingKeys(pages: FlowPage[], currentKey: string): string[]

Every page a visitor could go to next from currentKey — all route targets (regardless of gate, since we don’t know which will pass) plus the linear next. Used to prefetch the likely-next page chunks; not for navigation.

pageMeta

function · flow/spine.ts

pageMeta(meta: PageMeta): PageMeta

Identity helper for a page’s co-located metadata. export const meta = pageMeta({ type: 'paywall', next: s => … }).

parseSlotKey

function · flow/experiments.ts

parseSlotKey(key: string): { slot: string; variant?: string; }

Split a page key into its slot + optional variant: welcome@b{ slot, variant: 'b' }.

pickByWeight

function · flow/experiments.ts

pickByWeight(weights: Record<string, number>, u: number): string

Pick a variant key by weight from a stable unit position u ∈ [0,1). Variants are walked in declaration order and the first whose cumulative weight band contains u wins, so the same u always lands in the same variant.

resolveExperiments

function · flow/experiments.ts

resolveExperiments(pages: { key: string; }[], experiments: RuntimeExperiment[], seed: string | null): ExperimentResolution

Resolve which page version each experiment slot shows for this visitor, and which page keys remain in the linear flow.

Variant pages are detected structurally by the @ in their key, so they collapse out of the linear flow even for a slot with no active experiment. Each served experiment is RUNNING with all its arm pages guaranteed present in the LIVE build (the server draft-write + serve guards enforce the experiment-owned lifecycle invariant), so the visitor is bucketed deterministically on seed and the assigned arm’s page is swapped in with no presence check. With no seed there is nothing stable to bucket on, so every slot renders its own control page.

resolveRoute

function · flow/spine.ts

resolveRoute(routes: Route[] | undefined, s: FunnelSnapshot): string | undefined

Resolve an ordered list of routes to a target page key, or undefined to fall through to the linear next. First route whose gate passes (or has no when) wins.

BackProps

interface · flow/components.tsx

export interface BackProps extends ButtonHTMLAttributes<HTMLButtonElement> { children?: ReactNode }

ChoiceOption

interface · flow/components.tsx

export interface ChoiceOption { value: string label?: ReactNode }

The two-tier component model (doc 07 §“magic ≠ black box”): the magic lives in headless hooks (useField, useNavigation); these styleable components are thin, restyle-able wrappers over them. Every <Choice> reduces to useField

  • your own JSX — eject any time. They pass className/DOM props straight through and never dictate a pixel beyond the minimum.

ChoiceProps

interface · flow/components.tsx

export interface ChoiceProps { bind: string options: (string | ChoiceOption)[] className?: string selectedClassName?: string advanceOnSelect?: boolean }

Condition

interface · flow/spine.ts

export interface Condition { field: string op: ConditionOp value?: VariableValue | VariableValue[] }

A declarative gate over one namespaced field — UI-editable data the dashboard renders as a condition row (field op value). field is a dotted path into the snapshot: responses.goal, user.country, context.device.isMobile.

CreateFunnelTreeInput

interface · flow/mount.tsx

export interface CreateFunnelTreeInput { config: FunnelDefinition pages: RuntimePage[] layout?: ComponentType<{ children: ReactNode }> checkoutDriver?: (ctx: CheckoutDriverContext) => CheckoutDriver messages?: MessageCatalog }

ExperimentArm

interface · flow/experiments.ts

export interface ExperimentArm { page: string weight: number }

One arm of a running experiment: which page renders, at what weight.

ExperimentResolution

interface · flow/experiments.ts

export interface ExperimentResolution { assignments: Record<string, string> activeKeys: string[] render: Record<string, string> slotOf: Record<string, string> experimentOf: Record<string, string> versionOf: Record<string, number> }

FlowPage

interface · flow/spine.ts

export interface FlowPage { key: string meta?: PageMeta }

A page in the flow: its key + (optional) co-located meta.

FunnelDefinition

interface · flow/spine.ts

export interface FunnelDefinition { id: string pages?: FunnelPage[] checkout?: 'stripe' | 'paddle' responses?: Record<string, VariableConfig> data?: Record<string, VariableConfig> offerings?: string[] | Record<string, string | null> sampleOfferings?: Record<string, SampleOffering> locales?: FunnelLocales locationAwareCurrency?: boolean }

The funnel-level spine. Flow is mostly inferred from page file order + each page’s next; this holds the funnel-wide config. Stays thin — routing lives co-located on pages (doc 06: “funnel.ts stays thin”).

FunnelLocales

interface · flow/spine.ts

export interface FunnelLocales { default: string supported: string[] fallback?: string autoDetectLanguage?: boolean }

FunnelSnapshot

interface · flow/spine.ts

export interface FunnelSnapshot { user: Record<string, VariableValue> responses: Record<string, VariableValue> data: Record<string, VariableValue> context: FunnelContext }

Read-only view of every namespace a routing/condition predicate can branch on. user/responses/data are the writable namespaces; context is the read-only computed one (FunnelContext).

GroupProgress

interface · flow/spine.ts

export interface GroupProgress { groups: PageGroup[] activeIndex: number activeFraction: number }

The visitor-routed, branch-aware structure a grouped <ProgressBar> renders. See groupProgress. groups are ordered by first appearance along the routed path; activeIndex locates the current group (or -1 when the current page is ungrouped); activeFraction is the active group’s within-group fill (0–1).

MountOpts

interface · flow/mount.tsx

export interface MountOpts { offerings?: OfferingInput[] locale?: string mode?: 'test' | 'live' initialKey?: string basePath?: string assets?: Record<string, string> messages?: MessageCatalog visitorId?: string | null customerId?: string | null tracking?: { apiBase: string campaignId?: string | null preview?: { buildId: string token: string } | null funnelId: string experiment?: { id: string; variant: string; version?: number } route?: { id: string } } previewCheckout?: { apiBase: string funnelId: string buildId: string token: string } sessionValues?: Record<string, string | number | boolean | null | string[]> experiments?: RuntimeExperiment[] sessionId?: string | null embed?: boolean deferSessionValues?: boolean trustInitialKey?: boolean }

What the renderer injects when mounting a funnel build (SSR and hydration).

interface · flow/flow.tsx

export interface NavigationState { page: PageContext next: () => void back: () => void go: (key: string) => void prefetch: (key: string) => void nextCandidates: string[] canGoBack: boolean isNavigating: boolean }

NextProps

interface · flow/components.tsx

export interface NextProps extends ButtonHTMLAttributes<HTMLButtonElement> { children?: ReactNode }

PageGroup

interface · flow/spine.ts

export interface PageGroup { key: string pageCount: number completedCount: number active: boolean }

One segment of a grouped progress bar (a distinct PageMeta.group).

PageMeta

interface · flow/spine.ts

export interface PageMeta { type?: PageType slug?: string group?: string next?: Route[] guard?: Gate dynamic?: boolean prerender?: boolean }

Route

interface · flow/spine.ts

export interface Route { to: string when?: Gate }

One routing edge out of a page. to is a static target key (the dashboard reads these to draw the flow graph); when gates the edge (omit = fallback).

RuntimeExperiment

interface · flow/experiments.ts

export interface RuntimeExperiment { id: string slot: string variants: Record<string, ExperimentArm> metric?: string version?: number }

A runtime experiment record — the operational wiring the platform owns and hands to the SDK (not authored in the funnel source). Projected from the unified Experiment/ExperimentArm graph (a RUNNING PAGE experiment + its arms): the experiment’s slot names the base page, and each arm becomes a variants entry { page: arm.pageKey, weight: arm.weight } keyed by arm.label. The variant pages are code in the LIVE build; this is the live split over them.

The serve join only ever hands over RUNNING experiments, and the server’s draft-write + serve guards make every active arm’s page GUARANTEED present in the LIVE build (an experiment owns its arm pages’ lifecycle — you end the experiment to remove a page, you can’t drift it away). So there is no lifecycle/winner state and no “arm page missing” case to resolve here.

RuntimePage

interface · flow/flow.tsx

export interface RuntimePage { key: string meta?: PageMeta Component?: ComponentType load?: () => Promise<PageModule> }

A page wired into the runtime. Provide either an eager Component (small funnels, tests) or a lazy load (code-split: the renderer hands a dynamic import() so a big funnel ships one chunk per page, not the whole funnel).

ConditionOp

type · flow/spine.ts

export type ConditionOp = | 'eq' | 'neq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' | 'exists' | 'empty' | 'contains'

Gate

type · flow/spine.ts

export type Gate = Condition | Predicate

An edge gate: either declarative data (Condition) or a Predicate.

MountModule

type · flow/mount.tsx

export type MountModule = { pages: RuntimePage[] tree: (opts: MountOpts) => ReactNode }

The shape a funnel build’s mount.tsx module must export — the named, versioned contract between a build and the renderer/editor: import type { MountModule } from '@appfunnel-dev/sdk'.

PageComponent

type · flow/spine.ts

export type PageComponent<P extends object = Record<string, never>> = ComponentType<P>

A page component wired into the runtime — plain React that reaches the runtime through hooks. Kept as a named alias so the public type export stays stable.

PageModule

type · flow/flow.tsx

export type PageModule = ComponentType | { default: ComponentType }

What a page’s load resolves to — a component, or a module default-exporting one.

PageType

type · flow/spine.ts

export type PageType = | 'default' // plain content/step page (the baseline) | 'email-capture' // collects user.email — the mandatory identity step (validated) | 'paywall' // the offer/pricing page → drives paywall-view + paywall-CR | 'upsell' // post-purchase one-click → drives Upsell Rate + off-session charge | 'finish'

Predicate

type · flow/spine.ts

export type Predicate = (s: FunnelSnapshot) => boolean

A code-predicate gate — s => s.responses.goal === 'gain'.

AssetsContext

const · flow/mount.tsx

AssetsContext(props: import("/vercel/path0/node_modules/@types/react/index").ProviderProps<Record<string, string>>): ReactNode

Funnel asset map: logical name (e.g. logo.svg) → content-addressed CDN URL, injected by the renderer. Assets live in a SHARED content-addressed store (appfunnel/assets/<hash>), so identical content is stored once across all builds and funnels while staying immutable. Both SSR and client read the same injected map → URLs match. Defaults to {} (no provider needed in tests).

Last updated on