@appfunnel-dev/sdk 2.0.0-canary.0 → 2.0.0-canary.10
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.
- package/README.md +1 -1
- package/dist/capabilities-Dq22RCr_.d.cts +180 -0
- package/dist/capabilities-Dq22RCr_.d.ts +180 -0
- package/dist/checkout-ClaO5IYV.d.ts +333 -0
- package/dist/checkout-DBp4bCpC.d.cts +333 -0
- package/dist/chunk-7JLOF6CJ.cjs +172 -0
- package/dist/chunk-7JLOF6CJ.cjs.map +1 -0
- package/dist/chunk-EMMSS5I5.cjs +37 -0
- package/dist/chunk-EMMSS5I5.cjs.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/chunk-JAO6AA4R.js +99 -0
- package/dist/chunk-JAO6AA4R.js.map +1 -0
- package/dist/chunk-OXQBEKZ5.js +157 -0
- package/dist/chunk-OXQBEKZ5.js.map +1 -0
- package/dist/chunk-VV7TFC64.cjs +106 -0
- package/dist/chunk-VV7TFC64.cjs.map +1 -0
- package/dist/chunk-VW2HVPR4.js +446 -0
- package/dist/chunk-VW2HVPR4.js.map +1 -0
- package/dist/chunk-WYUDL4FI.cjs +8 -0
- package/dist/chunk-WYUDL4FI.cjs.map +1 -0
- package/dist/chunk-Y4YNJ2EX.cjs +476 -0
- package/dist/chunk-Y4YNJ2EX.cjs.map +1 -0
- package/dist/chunk-ZZJG4EYL.js +6 -0
- package/dist/chunk-ZZJG4EYL.js.map +1 -0
- package/dist/driver-paddle.cjs +817 -0
- package/dist/driver-paddle.cjs.map +1 -0
- package/dist/driver-paddle.d.cts +10 -0
- package/dist/driver-paddle.d.ts +10 -0
- package/dist/driver-paddle.js +814 -0
- package/dist/driver-paddle.js.map +1 -0
- package/dist/driver-stripe.cjs +2312 -0
- package/dist/driver-stripe.cjs.map +1 -0
- package/dist/driver-stripe.d.cts +24 -0
- package/dist/driver-stripe.d.ts +24 -0
- package/dist/driver-stripe.js +2305 -0
- package/dist/driver-stripe.js.map +1 -0
- package/dist/index.cjs +2654 -843
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +351 -1005
- package/dist/index.d.ts +351 -1005
- package/dist/index.js +2386 -704
- package/dist/index.js.map +1 -1
- package/dist/manifest-B3Tdab0M.d.cts +920 -0
- package/dist/manifest-B3Tdab0M.d.ts +920 -0
- package/dist/manifest-entry.cjs +408 -0
- package/dist/manifest-entry.cjs.map +1 -0
- package/dist/manifest-entry.d.cts +192 -0
- package/dist/manifest-entry.d.ts +192 -0
- package/dist/manifest-entry.js +270 -0
- package/dist/manifest-entry.js.map +1 -0
- package/dist/protocol.cjs +13 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +182 -0
- package/dist/protocol.d.ts +182 -0
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +49 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
1
2
|
import { ReactNode, ComponentType, ButtonHTMLAttributes } from 'react';
|
|
3
|
+
import { V as VariableValue, a as VariableConfig, A as Acquisition, O as Offering, D as Direction, F as FunnelContext, R as RuntimeExperiment, b as FunnelSnapshot, c as FunnelDefinition, d as buildContext, e as OfferingInput, P as PageMeta, f as PageContext } from './manifest-B3Tdab0M.js';
|
|
4
|
+
export { B as BrowserContext, g as BuildContextOptions, C as ClickIdContext, h as CompileInput, i as Condition, j as ConditionOp, k as DeviceContext, E as EdgeCondition, l as ExperimentArm, m as ExperimentResolution, n as FlowPage, o as FunnelLocales, p as FunnelManifest, G as Gate, I as IdentityContext, q as Interval, L as LocaleContext, M as ManifestEdge, r as ManifestPage, s as ManifestValidation, t as Money, u as OsContext, v as PageType, w as Predicate, x as ResolvedOffering, y as Route, S as SessionContext, z as SystemContext, U as UtmContext, H as assignVariant, J as bucketingSeed, K as buildAcquisition, N as buildCatalog, Q as compileManifest, T as currencyExponent, W as defineFunnel, X as definePage, Y as entryGuard, Z as evaluateCondition, _ as evaluateGate, $ as expectedPathLength, a0 as fnv1a, a1 as formatMoney, a2 as formatOffering, a3 as hashToUnit, a4 as isRtl, a5 as isVariantKey, a6 as nextPage, a7 as outgoingKeys, a8 as pageMeta, a9 as parseSlotKey, aa as pickByWeight, ab as resolveExperiments, ac as resolveLocale, ad as resolveOffering, ae as resolveRoute } from './manifest-B3Tdab0M.js';
|
|
5
|
+
import { NavSnapshot } from './protocol.js';
|
|
6
|
+
import { a as CheckoutDriver, C as CheckoutDriverContext } from './checkout-ClaO5IYV.js';
|
|
7
|
+
export { b as Checkout, c as CheckoutCallbacks, d as CheckoutHandle, e as CheckoutProps, f as CheckoutRequest, g as CheckoutResult, h as CheckoutResume, i as CheckoutStatus, F as FailureModalProps, j as FailureRoute, M as MockDriverOptions, O as OnFailedMap, U as Upsell, k as UpsellProps, l as UseCheckoutOptions, m as createMockDriver, n as driverWithEmail, u as useCheckout } from './checkout-ClaO5IYV.js';
|
|
2
8
|
import { ToasterProps } from 'sonner';
|
|
3
9
|
export { ToasterProps, toast } from 'sonner';
|
|
4
|
-
|
|
5
|
-
type VariableType = 'string' | 'number' | 'boolean' | 'stringArray';
|
|
6
|
-
type VariableValue = string | number | boolean | string[] | null | undefined;
|
|
7
|
-
interface VariableConfig {
|
|
8
|
-
type: VariableType;
|
|
9
|
-
default?: VariableValue;
|
|
10
|
-
persist?: boolean;
|
|
11
|
-
}
|
|
10
|
+
export { C as CheckoutError, a as CheckoutErrorCategory, b as CheckoutIntent, c as CheckoutProvider, d as CheckoutSurface, D as DeviceClass, I as INLINE_SURFACES, O as OffSessionReliability, P as PROVIDER_PROFILES, e as ProviderProfile, S as SurfaceCapability, f as SurfacePreference, U as UpsellKind, V as ValidationResult, g as checkoutError, i as isInlineSurface, h as isMerchantOfRecord, j as isOrchestrator, k as isRenderableSurface, r as renderableSurfaces, l as resolveSurface, s as surfacesFor, v as validateCheckout, m as validateUpsell } from './capabilities-Dq22RCr_.js';
|
|
12
11
|
|
|
13
12
|
declare class VariableStore {
|
|
14
13
|
private state;
|
|
@@ -70,198 +69,6 @@ interface FunnelStoreConfig {
|
|
|
70
69
|
*/
|
|
71
70
|
declare function createFunnelStore(config?: FunnelStoreConfig, sessionValues?: Record<string, VariableValue>): VariableStore;
|
|
72
71
|
|
|
73
|
-
/**
|
|
74
|
-
* v2 `context` — the **read-only, computed** namespace.
|
|
75
|
-
*
|
|
76
|
-
* Everything the funnel can *read* but never *write*: the device, the visitor's
|
|
77
|
-
* locale, the current page, acquisition params (UTM), the session, and system
|
|
78
|
-
* facts (funnel id, test/live mode, clock). It supersedes the v0 flat
|
|
79
|
-
* `page.*`/`device.*`/`browser.*`/`os.*`/`query.*`/`metadata.*` system variables
|
|
80
|
-
* (see {@link ./systemVariables}) by folding them into one nested object.
|
|
81
|
-
*
|
|
82
|
-
* Why it's *not* in the {@link ./variableStore}: the store holds **writable**
|
|
83
|
-
* state (`user`/`responses`/`data`) that persists + re-renders per key. Context
|
|
84
|
-
* is derived and read-only, so it lives as a plain object on a React context —
|
|
85
|
-
* no subscriptions, no churn. The one moving part (`page`) is merged into the
|
|
86
|
-
* snapshot from navigation state at read time.
|
|
87
|
-
*
|
|
88
|
-
* Authors read it through hooks (`useDevice()`, `useLocale()`, `usePage()`,
|
|
89
|
-
* `useUtm()`) or inside routing/condition predicates as `s.context.*`.
|
|
90
|
-
*/
|
|
91
|
-
interface DeviceContext {
|
|
92
|
-
type: 'mobile' | 'tablet' | 'desktop';
|
|
93
|
-
isMobile: boolean;
|
|
94
|
-
isTablet: boolean;
|
|
95
|
-
screenWidth: number;
|
|
96
|
-
screenHeight: number;
|
|
97
|
-
viewportWidth: number;
|
|
98
|
-
viewportHeight: number;
|
|
99
|
-
colorDepth: number;
|
|
100
|
-
pixelRatio: number;
|
|
101
|
-
}
|
|
102
|
-
interface BrowserContext {
|
|
103
|
-
name: string;
|
|
104
|
-
version: string;
|
|
105
|
-
userAgent: string;
|
|
106
|
-
language: string;
|
|
107
|
-
cookieEnabled: boolean;
|
|
108
|
-
online: boolean;
|
|
109
|
-
}
|
|
110
|
-
interface OsContext {
|
|
111
|
-
name: string;
|
|
112
|
-
timezone: string;
|
|
113
|
-
}
|
|
114
|
-
interface LocaleContext {
|
|
115
|
-
/** Full locale, e.g. `en-US`. */
|
|
116
|
-
locale: string;
|
|
117
|
-
/** Language subtag, e.g. `en`. */
|
|
118
|
-
language: string;
|
|
119
|
-
/** Region subtag, e.g. `US` (may be empty). */
|
|
120
|
-
region: string;
|
|
121
|
-
/** IANA timezone, e.g. `America/New_York`. */
|
|
122
|
-
timeZone: string;
|
|
123
|
-
}
|
|
124
|
-
interface PageContext {
|
|
125
|
-
/** Current page key. */
|
|
126
|
-
key: string;
|
|
127
|
-
/** Current page URL slug (`meta.slug`, defaults to the key) — what the renderer puts in the path. */
|
|
128
|
-
slug: string;
|
|
129
|
-
/** 0-based position in the visited history. */
|
|
130
|
-
index: number;
|
|
131
|
-
/** Total pages in the (expected) flow. */
|
|
132
|
-
total: number;
|
|
133
|
-
/** 0–100. */
|
|
134
|
-
progressPercentage: number;
|
|
135
|
-
/** Epoch ms the current page was entered. */
|
|
136
|
-
startedAt: number;
|
|
137
|
-
}
|
|
138
|
-
/** Acquisition / ad params captured on entry (the old `query.*` utm slice). */
|
|
139
|
-
interface UtmContext {
|
|
140
|
-
source?: string;
|
|
141
|
-
medium?: string;
|
|
142
|
-
campaign?: string;
|
|
143
|
-
content?: string;
|
|
144
|
-
term?: string;
|
|
145
|
-
[key: string]: string | undefined;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Per-network ad **click-ids** captured from the entry URL — the matching signals
|
|
149
|
-
* the server CAPI needs. Named per network (not a generic bag) so the platform +
|
|
150
|
-
* each integration map them precisely; `[key]` catches any others.
|
|
151
|
-
*/
|
|
152
|
-
interface ClickIdContext {
|
|
153
|
-
/** Meta / Facebook */
|
|
154
|
-
fbclid?: string;
|
|
155
|
-
/** Google Ads (+ gbraid/wbraid for app↔web) */
|
|
156
|
-
gclid?: string;
|
|
157
|
-
gbraid?: string;
|
|
158
|
-
wbraid?: string;
|
|
159
|
-
/** Microsoft / Bing */
|
|
160
|
-
msclkid?: string;
|
|
161
|
-
/** TikTok */
|
|
162
|
-
ttclid?: string;
|
|
163
|
-
/** Twitter / X */
|
|
164
|
-
twclid?: string;
|
|
165
|
-
/** Snapchat */
|
|
166
|
-
sccid?: string;
|
|
167
|
-
/** LinkedIn */
|
|
168
|
-
li_fat_id?: string;
|
|
169
|
-
/** Pinterest */
|
|
170
|
-
epik?: string;
|
|
171
|
-
/** Reddit */
|
|
172
|
-
rdt_cid?: string;
|
|
173
|
-
[key: string]: string | undefined;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* The full acquisition snapshot captured once on entry — the funnel reads `utm` /
|
|
177
|
-
* `clickIds` off {@link FunnelContext}; the SDK forwards this whole thing to the
|
|
178
|
-
* tracker (`setAcquisition`) so the platform persists it **first-class**: per-visit
|
|
179
|
-
* on the session + **first-touch (write-once) on the Customer**, and the click-ids /
|
|
180
|
-
* `_fbp`/`_fbc` cookies feed CAPI. Not stored as a generic attribute/answer.
|
|
181
|
-
*/
|
|
182
|
-
interface Acquisition {
|
|
183
|
-
utm: UtmContext;
|
|
184
|
-
clickIds: ClickIdContext;
|
|
185
|
-
/** CAPI cookies (`_fbp`/`_fbc`) read from `document.cookie` (not funnel-readable). */
|
|
186
|
-
cookies: {
|
|
187
|
-
fbp?: string;
|
|
188
|
-
fbc?: string;
|
|
189
|
-
};
|
|
190
|
-
/** `document.referrer` at entry. */
|
|
191
|
-
referrer: string;
|
|
192
|
-
/** Entry URL path (the landing page). */
|
|
193
|
-
landingPath: string;
|
|
194
|
-
}
|
|
195
|
-
interface SessionContext {
|
|
196
|
-
/** Server session id; null until resolved. */
|
|
197
|
-
id: string | null;
|
|
198
|
-
/** Epoch ms the session began. */
|
|
199
|
-
startedAt: number;
|
|
200
|
-
/** `document.referrer` at entry. */
|
|
201
|
-
referrer: string;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Durable identity for **stable experiment bucketing** (phase-4 §4.2). The
|
|
205
|
-
* preferred seed is `customerId` once known; before identification it's the
|
|
206
|
-
* signed anonymous `visitorId` (P1). It is deliberately **not** the session id or
|
|
207
|
-
* fingerprint — a returning visitor (e.g. via winback) must keep their variant
|
|
208
|
-
* across sessions/devices (landmine #4). Both null = unidentifiable traffic, and
|
|
209
|
-
* the experiment layer falls back to control rather than bucket on nothing.
|
|
210
|
-
*/
|
|
211
|
-
interface IdentityContext {
|
|
212
|
-
/** Known customer id (post-identification). The preferred bucketing seed. */
|
|
213
|
-
customerId: string | null;
|
|
214
|
-
/** Signed anonymous visitor id. The fallback bucketing seed. */
|
|
215
|
-
visitorId: string | null;
|
|
216
|
-
}
|
|
217
|
-
interface SystemContext {
|
|
218
|
-
funnelId: string;
|
|
219
|
-
campaignId: string;
|
|
220
|
-
/** Render mode — `test` for preview/QA, `live` for public traffic. */
|
|
221
|
-
mode: 'test' | 'live';
|
|
222
|
-
/** Epoch ms captured at build time (stable per render, not a live clock). */
|
|
223
|
-
now: number;
|
|
224
|
-
}
|
|
225
|
-
/** The whole read-only computed namespace, as read in predicates: `s.context.*`. */
|
|
226
|
-
interface FunnelContext {
|
|
227
|
-
device: DeviceContext;
|
|
228
|
-
browser: BrowserContext;
|
|
229
|
-
os: OsContext;
|
|
230
|
-
locale: LocaleContext;
|
|
231
|
-
page: PageContext;
|
|
232
|
-
utm: UtmContext;
|
|
233
|
-
/** Per-network ad click-ids captured on entry (funnel-readable; also forwarded for CAPI). */
|
|
234
|
-
clickIds: ClickIdContext;
|
|
235
|
-
session: SessionContext;
|
|
236
|
-
identity: IdentityContext;
|
|
237
|
-
system: SystemContext;
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Assemble the full {@link Acquisition} snapshot from the context + `document`
|
|
241
|
-
* (cookies, referrer, path) — captured once on entry, forwarded to the tracker for
|
|
242
|
-
* first-class persistence (session + first-touch Customer) + CAPI.
|
|
243
|
-
*/
|
|
244
|
-
declare function buildAcquisition(context: FunnelContext): Acquisition;
|
|
245
|
-
interface BuildContextOptions {
|
|
246
|
-
funnelId?: string;
|
|
247
|
-
campaignId?: string;
|
|
248
|
-
mode?: 'test' | 'live';
|
|
249
|
-
/** Stable build-time clock (avoids `Date.now()` churn in renders/tests). */
|
|
250
|
-
now?: number;
|
|
251
|
-
sessionId?: string | null;
|
|
252
|
-
/** Known customer id — the preferred stable bucketing seed (phase-4 §4.2). */
|
|
253
|
-
customerId?: string | null;
|
|
254
|
-
/** Signed anonymous visitor id — the fallback bucketing seed. */
|
|
255
|
-
visitorId?: string | null;
|
|
256
|
-
/** Overrides for any slice — used by the server to hydrate `utm`/`session`. */
|
|
257
|
-
overrides?: Partial<FunnelContext>;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Build the read-only {@link FunnelContext} once, at mount. `page` starts at the
|
|
261
|
-
* first page and is replaced per navigation when assembling a snapshot.
|
|
262
|
-
*/
|
|
263
|
-
declare function buildContext(opts?: BuildContextOptions): FunnelContext;
|
|
264
|
-
|
|
265
72
|
/**
|
|
266
73
|
* v2 tracking seam — the funnel runtime emits a **fixed event taxonomy**, but
|
|
267
74
|
* never talks to the network itself. It calls a {@link Tracker} the platform/CLI
|
|
@@ -306,8 +113,17 @@ interface FunnelEventDataMap {
|
|
|
306
113
|
productId?: string;
|
|
307
114
|
priceId?: string;
|
|
308
115
|
productName?: string;
|
|
116
|
+
surface?: string;
|
|
117
|
+
amount?: number;
|
|
118
|
+
currency?: string;
|
|
119
|
+
eventId?: string;
|
|
120
|
+
};
|
|
121
|
+
'checkout.payment_added': {
|
|
122
|
+
productId?: string;
|
|
123
|
+
amount?: number;
|
|
124
|
+
currency?: string;
|
|
125
|
+
eventId?: string;
|
|
309
126
|
};
|
|
310
|
-
'checkout.payment_added': Record<string, never>;
|
|
311
127
|
'checkout.failed': {
|
|
312
128
|
category: string;
|
|
313
129
|
declineCode?: string;
|
|
@@ -319,10 +135,18 @@ interface FunnelEventDataMap {
|
|
|
319
135
|
currency?: string;
|
|
320
136
|
email?: string;
|
|
321
137
|
eventId?: string;
|
|
138
|
+
productId?: string;
|
|
139
|
+
};
|
|
140
|
+
'subscription.created': {
|
|
141
|
+
amount?: number;
|
|
142
|
+
currency?: string;
|
|
143
|
+
productId?: string;
|
|
144
|
+
eventId?: string;
|
|
322
145
|
};
|
|
323
146
|
'experiment.exposure': {
|
|
324
147
|
experimentId: string;
|
|
325
148
|
variant: string;
|
|
149
|
+
version?: number;
|
|
326
150
|
};
|
|
327
151
|
}
|
|
328
152
|
declare function newEventId(): string;
|
|
@@ -364,504 +188,123 @@ declare function useTracker(): {
|
|
|
364
188
|
};
|
|
365
189
|
|
|
366
190
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* **AppFunnel does not manage currency.** Multi-currency is entirely the payment
|
|
370
|
-
* provider's job: the provider (or its preview API) resolves the visitor's price,
|
|
371
|
-
* and we receive a single resolved `{ amount, currency }` per product. No FX, no
|
|
372
|
-
* currency selection, no conversion on our side. (FX appears only for frozen-USD
|
|
373
|
-
* *reporting*, never display — phase-3 §3.6b.) A provider that doesn't localize
|
|
374
|
-
* simply gives one currency.
|
|
375
|
-
*
|
|
376
|
-
* Our only job is **display**: pick the product by logical id and **format** its
|
|
377
|
-
* amount. Formatting happens at read time in the funnel's **active locale** (so
|
|
378
|
-
* switching language reformats `kr 79,00` ⇄ `$9.99`), while the **currency** is
|
|
379
|
-
* whatever the provider resolved. Currency selection is the platform's; the
|
|
380
|
-
* authoring funnel only ever names a product id.
|
|
381
|
-
*/
|
|
382
|
-
|
|
383
|
-
type Interval = 'day' | 'week' | 'month' | 'year' | 'one_time';
|
|
384
|
-
/** What the platform hands the funnel per product (a single provider-resolved price). */
|
|
385
|
-
interface ProductInput {
|
|
386
|
-
id: string;
|
|
387
|
-
name?: string;
|
|
388
|
-
displayName?: string;
|
|
389
|
-
/** Provider-resolved price in **minor units** (the provider chose the currency). */
|
|
390
|
-
amount: number;
|
|
391
|
-
/** Currency of `amount`, as the provider resolved it (e.g. `DKK`). */
|
|
392
|
-
currency: string;
|
|
393
|
-
interval?: Interval;
|
|
394
|
-
intervalCount?: number;
|
|
395
|
-
trialDays?: number;
|
|
396
|
-
/** Trial price in minor units (omit/0 = free trial). */
|
|
397
|
-
trialAmount?: number;
|
|
398
|
-
/** Settlement provider — opaque to the author, used by the checkout driver. */
|
|
399
|
-
provider?: string;
|
|
400
|
-
}
|
|
401
|
-
/** A money value with a locale-formatted string. */
|
|
402
|
-
interface Money {
|
|
403
|
-
/** Major units, e.g. `79`. */
|
|
404
|
-
amount: number;
|
|
405
|
-
/** Minor units, e.g. `7900`. */
|
|
406
|
-
minor: number;
|
|
407
|
-
currency: string;
|
|
408
|
-
/** Locale-formatted, e.g. `kr 79,00` / `€9,99` / `$9.99`. */
|
|
409
|
-
formatted: string;
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* The product with its amounts resolved (currency + period math), **before**
|
|
413
|
-
* locale formatting — what the catalog stores. {@link useProduct} formats it.
|
|
414
|
-
*/
|
|
415
|
-
interface ResolvedProduct {
|
|
416
|
-
id: string;
|
|
417
|
-
name: string;
|
|
418
|
-
displayName: string;
|
|
419
|
-
provider: string;
|
|
420
|
-
currency: string;
|
|
421
|
-
priceMinor: number;
|
|
422
|
-
perDayMinor: number;
|
|
423
|
-
perWeekMinor: number;
|
|
424
|
-
perMonthMinor: number;
|
|
425
|
-
perYearMinor: number;
|
|
426
|
-
period: string;
|
|
427
|
-
periodly: string;
|
|
428
|
-
hasTrial: boolean;
|
|
429
|
-
trialDays: number;
|
|
430
|
-
trialMinor: number;
|
|
431
|
-
}
|
|
432
|
-
/** Display-ready product the author reads (every amount locale-formatted). */
|
|
433
|
-
interface Product {
|
|
434
|
-
id: string;
|
|
435
|
-
name: string;
|
|
436
|
-
displayName: string;
|
|
437
|
-
provider: string;
|
|
438
|
-
price: Money;
|
|
439
|
-
/** e.g. `month`, `quarter`, `one-time`. */
|
|
440
|
-
period: string;
|
|
441
|
-
/** e.g. `monthly`, `quarterly`, `one-time`. */
|
|
442
|
-
periodly: string;
|
|
443
|
-
/** Period-normalized prices (for "just $0.27/day" style copy). */
|
|
444
|
-
perDay: Money;
|
|
445
|
-
perWeek: Money;
|
|
446
|
-
perMonth: Money;
|
|
447
|
-
perYear: Money;
|
|
448
|
-
hasTrial: boolean;
|
|
449
|
-
trialDays: number;
|
|
450
|
-
trialPrice: Money | null;
|
|
451
|
-
}
|
|
452
|
-
/** Resolve a {@link ProductInput} into amounts + period math (no formatting yet). */
|
|
453
|
-
declare function resolveProduct(input: ProductInput): ResolvedProduct;
|
|
454
|
-
/** Build the id → {@link ResolvedProduct} catalog. Formatting happens at read time. */
|
|
455
|
-
declare function buildCatalog(inputs: ProductInput[]): Map<string, ResolvedProduct>;
|
|
456
|
-
/** Format a {@link ResolvedProduct} into a display {@link Product} in `locale`. */
|
|
457
|
-
declare function formatProduct(r: ResolvedProduct, locale: string): Product;
|
|
458
|
-
/** `useProduct('monthly')` — one product, formatted in the active locale. */
|
|
459
|
-
declare function useProduct(id: string | undefined): Product | undefined;
|
|
460
|
-
/** `useProducts()` — every product, formatted in the active locale, in order. */
|
|
461
|
-
declare function useProducts(): Product[];
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* v2 commerce primitives — the two intents from phase-3, made trivial.
|
|
465
|
-
*
|
|
466
|
-
* - `<Checkout product surface>` — the **purchase** primitive. The `surface`
|
|
467
|
-
* prop picks presentation; some surfaces render **inline** (express wallets,
|
|
468
|
-
* inline card, provider iframe), some **open on click** (sheet/popup/redirect).
|
|
469
|
-
* `<Checkout>` covers both: it renders inline surfaces and renders a trigger
|
|
470
|
-
* button for the rest (`asChild` to use your own button).
|
|
471
|
-
* - `<Upsell product>` — the **upsell** primitive. Off-session charge to the
|
|
472
|
-
* saved method, no card re-entry, one tap.
|
|
473
|
-
* - `useCheckout()` — the headless hook under both, always available for a
|
|
474
|
-
* fully-custom CTA.
|
|
191
|
+
* The **`window.appfunnel` event bus** — the client-side integration surface
|
|
192
|
+
* (port of v0 `admin/lib/funnel/utils/appfunnelRuntime.ts`).
|
|
475
193
|
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* (
|
|
194
|
+
* Every taxonomy event the runtime emits is mirrored here, so on-page pixels and
|
|
195
|
+
* custom scripts integrate by **subscribing** — `window.appfunnel.on('purchase.complete', …)`
|
|
196
|
+
* — instead of the funnel hand-injecting pixel config per page. The pre-built pixel
|
|
197
|
+
* loaders (Meta/TikTok/GTM/Clarity) are just subscribers; CAPI stays the server-side
|
|
198
|
+
* default (this is the *client* sink of the same events). See doc 08 §3.
|
|
480
199
|
*
|
|
481
|
-
*
|
|
482
|
-
* (
|
|
200
|
+
* Two deliberate properties vs. v0:
|
|
201
|
+
* - **Replay history** (`events`): async pixels load *after* `funnel.start`/`page.view`
|
|
202
|
+
* fire, so a late subscriber would miss them. The bus keeps a bounded history a
|
|
203
|
+
* subscriber can replay (the GTM `dataLayer` trick) — no lost events.
|
|
204
|
+
* - **Subscribe + read + drive.** The window surface exposes `on`/`off`/`events` + read
|
|
205
|
+
* getters, and — resolving the deferred bus-control-scope decision (doc 08 §3/§4) —
|
|
206
|
+
* **flow control**: `goTo`/`goNext`/`goPrevious` + a `navigation()` snapshot, so a host
|
|
207
|
+
* page (the live editor, or an author's own external nav UI) can drive the funnel. It
|
|
208
|
+
* still has **no `emit`** (third-party code can't forge taxonomy events) and no
|
|
209
|
+
* `setVariable` (state stays funnel-owned). Flow control is a no-op (warns once) until a
|
|
210
|
+
* `<FunnelView>` mounts and registers itself via {@link FunnelBus.registerNavigation}.
|
|
483
211
|
*/
|
|
484
212
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
* The kind of off-session upsell (researched against Funnelfox's changelog, which
|
|
493
|
-
* ships these as distinct features). The discriminating real-world constraint is
|
|
494
|
-
* **stacking a 2nd concurrent subscription** — Paddle can't, SolidGate/Stripe can.
|
|
495
|
-
*/
|
|
496
|
-
type UpsellKind = 'subscription' | 'one-time' | 'upgrade';
|
|
497
|
-
/** Reliability of an off-session upsell charge AFTER a purchase on a surface. */
|
|
498
|
-
type OffSessionReliability = 'reliable' | 'conditional' | 'none';
|
|
499
|
-
interface SurfaceCapability {
|
|
500
|
-
purchase: boolean;
|
|
501
|
-
/** Apple/Google Pay available on this surface. */
|
|
502
|
-
wallets: boolean;
|
|
503
|
-
/** Reliability of an off-session upsell after a purchase on this surface. */
|
|
504
|
-
offSession: OffSessionReliability;
|
|
505
|
-
}
|
|
506
|
-
interface ProviderProfile {
|
|
507
|
-
/** Merchant-of-Record (handles tax/compliance) vs. a PSP you settle through. */
|
|
508
|
-
isMoR: boolean;
|
|
509
|
-
/** Upsell kinds this provider can charge off-session (one-click, no re-entry). */
|
|
510
|
-
offSessionUpsells: UpsellKind[];
|
|
511
|
-
/** Who charges the saved method off-session: you (PSP) or the provider (MoR/managed). */
|
|
512
|
-
tokenModel: 'merchant' | 'provider';
|
|
513
|
-
/** Routes across multiple processors (Primer; SolidGate also offers this) vs. a single acquirer. */
|
|
514
|
-
orchestrator: boolean;
|
|
515
|
-
/** Supported surfaces → per-surface capabilities. **Absent = not supported.** */
|
|
516
|
-
surfaces: Partial<Record<CheckoutSurface, SurfaceCapability>>;
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Provider capabilities (researched 2026-06-23 against provider docs + Funnelfox).
|
|
520
|
-
*
|
|
521
|
-
* Three real-world corrections to the naive model:
|
|
522
|
-
* 1. **Off-session upsell support is per-provider by *kind*** (`offSessionUpsells`):
|
|
523
|
-
* Paddle can't stack a 2nd concurrent subscription (upgrade + one-time only);
|
|
524
|
-
* Stripe/SolidGate/Whop/Primer can do all kinds.
|
|
525
|
-
* 2. **Off-session *reliability* varies by the paywall surface** (`SurfaceCapability.
|
|
526
|
-
* offSession`): Stripe **managed** checkout (`embedded`/`redirect`) upsells as a
|
|
527
|
-
* direct charge against Stripe's vault → `conditional` (may decline); Elements
|
|
528
|
-
* surfaces are `reliable`.
|
|
529
|
-
* 3. **Surface availability is a hard constraint.** MoR/managed providers can't
|
|
530
|
-
* expose a raw card field to your own UI: Paddle has no `express`/`element`/your
|
|
531
|
-
* `sheet`; Whop has no raw `element`/`express`; Primer has no hosted `redirect`;
|
|
532
|
-
* Stripe/SolidGate have no `popup`. A missing entry means "not possible."
|
|
533
|
-
*/
|
|
534
|
-
declare const PROVIDER_PROFILES: Record<CheckoutProvider, ProviderProfile>;
|
|
535
|
-
interface ValidationResult {
|
|
536
|
-
ok: boolean;
|
|
537
|
-
reason?: string;
|
|
538
|
-
/** Non-fatal caveat (e.g. Paddle upgrade-only). */
|
|
539
|
-
note?: string;
|
|
540
|
-
}
|
|
541
|
-
/** Surfaces a provider supports, for the editor/AI to steer authors. */
|
|
542
|
-
declare function surfacesFor(provider: CheckoutProvider): CheckoutSurface[];
|
|
543
|
-
/** Whether a provider is a Merchant of Record (it owns tax/compliance). */
|
|
544
|
-
declare function isMerchantOfRecord(provider: CheckoutProvider): boolean;
|
|
545
|
-
/** Whether a provider routes across multiple processors (Primer, SolidGate). */
|
|
546
|
-
declare function isOrchestrator(provider: CheckoutProvider): boolean;
|
|
547
|
-
/**
|
|
548
|
-
* Validate a (provider, surface) for a **purchase**: the surface must exist for
|
|
549
|
-
* the provider and accept a payment. Surface availability is the real constraint
|
|
550
|
-
* (e.g. Paddle has no `element`).
|
|
551
|
-
*/
|
|
552
|
-
declare function validateCheckout(provider: CheckoutProvider, surface: CheckoutSurface): ValidationResult;
|
|
553
|
-
/**
|
|
554
|
-
* Validate an **off-session upsell** of `kind` after a purchase made on
|
|
555
|
-
* `paywallSurface`. Combines provider support for the kind (Paddle can't stack a
|
|
556
|
-
* 2nd subscription) with the paywall surface's off-session reliability (Stripe
|
|
557
|
-
* managed checkout is `conditional` — returned as a `note`, not a hard block).
|
|
558
|
-
*/
|
|
559
|
-
declare function validateUpsell(provider: CheckoutProvider, paywallSurface: CheckoutSurface, kind?: UpsellKind): ValidationResult;
|
|
560
|
-
type CheckoutStatus = 'idle' | 'loading' | 'requires_action' | 'success' | 'error';
|
|
561
|
-
interface CheckoutRequest {
|
|
562
|
-
product: string;
|
|
563
|
-
intent: CheckoutIntent;
|
|
564
|
-
surface?: CheckoutSurface;
|
|
565
|
-
/** For `upsell` intent — which kind of off-session charge (default `subscription`). */
|
|
566
|
-
kind?: UpsellKind;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Normalized failure category — the driver/platform maps each provider's decline
|
|
570
|
-
* codes into one of these so funnels can route/recover by reason. `authentication_
|
|
571
|
-
* required` (3DS/SCA) and `requires_payment_method` are the cases an **off-session**
|
|
572
|
-
* upsell can't satisfy → recover on-session (re-collect a card).
|
|
573
|
-
*/
|
|
574
|
-
type CheckoutErrorCategory = 'card_declined' | 'insufficient_funds' | 'expired_card' | 'authentication_required' | 'requires_payment_method' | 'processing_error' | 'canceled' | 'unknown';
|
|
575
|
-
interface CheckoutError {
|
|
576
|
-
category: CheckoutErrorCategory;
|
|
577
|
-
message: string;
|
|
578
|
-
/** Raw provider decline code, if any (e.g. Stripe `card_declined`/`insufficient_funds`). */
|
|
579
|
-
declineCode?: string;
|
|
580
|
-
/** Whether re-attempting (e.g. an on-session re-collection) may succeed. */
|
|
581
|
-
retryable: boolean;
|
|
213
|
+
/** Read accessors the bus exposes — wired to the funnel's store + context. */
|
|
214
|
+
interface BusAccessors {
|
|
215
|
+
getVariable: (key: string) => unknown;
|
|
216
|
+
getVariables: () => Record<string, unknown>;
|
|
217
|
+
getCurrentPageId: () => string | null;
|
|
218
|
+
getCustomerId: () => string | null;
|
|
219
|
+
getVisitorId: () => string | null;
|
|
582
220
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
interface CheckoutResult {
|
|
589
|
-
ok: boolean;
|
|
590
|
-
error?: CheckoutError;
|
|
591
|
-
/** Charged amount in minor units (for the purchase event). */
|
|
592
|
-
amountMinor?: number;
|
|
593
|
-
currency?: string;
|
|
594
|
-
/**
|
|
595
|
-
* The **server-minted** dedup id for the purchase event — the platform creates it
|
|
596
|
-
* when the charge settles and fires CAPI with it; the SDK emits `purchase.complete`
|
|
597
|
-
* with the same id so the browser pixel dedupes against the server. (Server is the
|
|
598
|
-
* source of truth for payment events; the SDK doesn't mint these.)
|
|
599
|
-
*/
|
|
600
|
-
eventId?: string;
|
|
221
|
+
interface BusEvent {
|
|
222
|
+
event: string;
|
|
223
|
+
data: unknown;
|
|
224
|
+
/** Epoch ms the event was emitted (for ordering / replay). */
|
|
225
|
+
ts: number;
|
|
601
226
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
*/
|
|
611
|
-
onPaymentAdded?: () => void;
|
|
227
|
+
type Listener = (data: unknown) => void;
|
|
228
|
+
/** The flow-control seam a mounted `<FunnelView>` registers into the bus (SDK-internal). */
|
|
229
|
+
interface NavigationControl {
|
|
230
|
+
/** Jump to a page key. Returns `false` if the key isn't a real page (no navigation happened). */
|
|
231
|
+
goTo(pageKey: string): boolean;
|
|
232
|
+
goNext(): void;
|
|
233
|
+
goPrevious(): void;
|
|
234
|
+
snapshot(): NavSnapshot | null;
|
|
612
235
|
}
|
|
613
236
|
/**
|
|
614
|
-
* The
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* Most providers ship their **own React SDK** (`@stripe/react-stripe-js`,
|
|
618
|
-
* `@paddle/paddle-js`, Primer's React components, …). A driver's `renderInline`
|
|
619
|
-
* returns exactly that: the provider's React element mounted in our chrome (inline,
|
|
620
|
-
* or inside the `<Sheet>`). Those provider SDKs live in the **driver/platform**
|
|
621
|
-
* package, not our core funnel SDK — which is why the driver is injected: the core
|
|
622
|
-
* stays lean and provider-agnostic, and you only pull the SDK for the provider you use.
|
|
237
|
+
* The public `window.appfunnel` surface — subscribe + read + drive the flow. It never
|
|
238
|
+
* exposes `emit` (no forged events) or `setVariable` (state stays funnel-owned).
|
|
623
239
|
*/
|
|
624
|
-
interface
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
declare function createMockDriver(provider?: CheckoutProvider, options?: MockDriverOptions): CheckoutDriver;
|
|
639
|
-
interface UseCheckoutOptions {
|
|
640
|
-
intent?: CheckoutIntent;
|
|
641
|
-
/** For `upsell` intent — which kind of off-session charge (default `subscription`). */
|
|
642
|
-
kind?: UpsellKind;
|
|
643
|
-
/** Advance to the next page after a successful charge (default: true). */
|
|
644
|
-
advanceOnSuccess?: boolean;
|
|
645
|
-
onSuccess?: (result: CheckoutResult) => void;
|
|
646
|
-
onError?: (error: CheckoutError) => void;
|
|
647
|
-
}
|
|
648
|
-
interface CheckoutHandle {
|
|
649
|
-
/** Start the charge for a product; resolves with the result (no surface = off-session). */
|
|
650
|
-
open: (product: string, surface?: CheckoutSurface) => Promise<CheckoutResult>;
|
|
651
|
-
status: CheckoutStatus;
|
|
652
|
-
/** The last failure, structured so you can route/recover by `category`. */
|
|
653
|
-
error: CheckoutError | null;
|
|
654
|
-
isLoading: boolean;
|
|
655
|
-
reset: () => void;
|
|
656
|
-
/** Build the callbacks an inline surface reports through (used by `<Checkout>`). */
|
|
657
|
-
callbacksFor: (product: string, surface?: CheckoutSurface) => CheckoutCallbacks & {
|
|
658
|
-
onStart: () => void;
|
|
659
|
-
};
|
|
240
|
+
interface AppfunnelBusApi extends BusAccessors {
|
|
241
|
+
/** Subscribe to an event (or `'*'` for all). Returns an unsubscribe fn. */
|
|
242
|
+
on(event: string, cb: Listener): () => void;
|
|
243
|
+
off(event: string, cb: Listener): void;
|
|
244
|
+
/** Bounded replay history — read past events, then `on()` for future ones. */
|
|
245
|
+
readonly events: BusEvent[];
|
|
246
|
+
/** Jump the flow to a page key. Returns `false` for an unknown key (or before the flow mounts). */
|
|
247
|
+
goTo(pageKey: string): boolean;
|
|
248
|
+
/** Advance via the current page's `next` predicate (else the linear next). */
|
|
249
|
+
goNext(): void;
|
|
250
|
+
/** Step back to the previous page in flow history. */
|
|
251
|
+
goPrevious(): void;
|
|
252
|
+
/** The current flow snapshot, or `null` before a `<FunnelView>` mounts. */
|
|
253
|
+
navigation(): NavSnapshot | null;
|
|
660
254
|
}
|
|
661
255
|
/**
|
|
662
|
-
* The
|
|
663
|
-
*
|
|
256
|
+
* The internal bus: the public {@link AppfunnelBusApi} plus `emit` (SDK-only) and
|
|
257
|
+
* `now` injection. The SDK holds this; only {@link AppfunnelBusApi} reaches `window`.
|
|
664
258
|
*/
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
|
|
669
|
-
surface: CheckoutSurface;
|
|
670
|
-
/** Render your own trigger element instead of the default button (trigger surfaces). */
|
|
671
|
-
asChild?: boolean;
|
|
672
|
-
/** Trigger label / your trigger element. */
|
|
673
|
-
children?: ReactNode;
|
|
674
|
-
className?: string;
|
|
259
|
+
interface FunnelBus extends AppfunnelBusApi {
|
|
260
|
+
emit(event: string, data?: unknown): void;
|
|
261
|
+
/** SDK-only: a mounted `<FunnelView>` registers its flow control; returns an unregister fn. */
|
|
262
|
+
registerNavigation(nav: NavigationControl): () => void;
|
|
675
263
|
}
|
|
264
|
+
/** Create a bus over the given read accessors. `now` is injectable for tests. */
|
|
265
|
+
declare function createBus(accessors: BusAccessors, now?: () => number): FunnelBus;
|
|
676
266
|
/**
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* - **SDK sheet** (`sheet`) — a trigger opens the AppFunnel-owned {@link Sheet}
|
|
681
|
-
* and the driver renders the provider element inside it;
|
|
682
|
-
* - **provider-hosted** (`popup`/`redirect`) — a trigger hands off to the
|
|
683
|
-
* provider's own overlay/page.
|
|
267
|
+
* Wrap a {@link Tracker} so every emitted taxonomy event is mirrored to the bus
|
|
268
|
+
* (the client sink). `identify()` also surfaces a `user.registered` to the bus, so
|
|
269
|
+
* pixels see registration. The server sink (the inner tracker → CAPI) is untouched.
|
|
684
270
|
*/
|
|
685
|
-
declare function
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
* re-collect a card and charge again — e.g. `fallback="sheet"` opens the
|
|
691
|
-
* payment sheet, `fallback="popup"` the provider overlay. Omit to just report
|
|
692
|
-
* the failure (via `onError` / the `checkout.failed` event).
|
|
693
|
-
*/
|
|
694
|
-
fallback?: CheckoutSurface;
|
|
695
|
-
asChild?: boolean;
|
|
696
|
-
children?: ReactNode;
|
|
697
|
-
className?: string;
|
|
271
|
+
declare function withBus(tracker: Tracker, bus: FunnelBus): Tracker;
|
|
272
|
+
declare global {
|
|
273
|
+
interface Window {
|
|
274
|
+
appfunnel?: AppfunnelBusApi;
|
|
275
|
+
}
|
|
698
276
|
}
|
|
699
277
|
/**
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* surface to re-collect and retry.
|
|
278
|
+
* Attach the bus's public surface to `window.appfunnel` (client-side only).
|
|
279
|
+
* Returns a detach fn for unmount. No-op under SSR.
|
|
703
280
|
*/
|
|
704
|
-
declare function
|
|
281
|
+
declare function attachBus(bus: FunnelBus): () => void;
|
|
705
282
|
|
|
706
283
|
/**
|
|
707
|
-
*
|
|
708
|
-
* code (`defineFunnel` / `pageMeta` / `definePage`) yet **dashboard-editable**
|
|
709
|
-
* (static AST parse → pretty UI → codegen) and compiled to the manifest IR.
|
|
284
|
+
* v2 `offerings` — the **read-only** offering catalog.
|
|
710
285
|
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
286
|
+
* **Appfunnel does not manage currency.** Multi-currency is entirely the payment
|
|
287
|
+
* provider's job: the provider (or its preview API) resolves the visitor's price,
|
|
288
|
+
* and we receive a single resolved `{ amount, currency }` per offering. No FX, no
|
|
289
|
+
* currency selection, no conversion on our side. (FX appears only for frozen-USD
|
|
290
|
+
* *reporting*, never display — phase-3 §3.6b.) A provider that doesn't localize
|
|
291
|
+
* simply gives one currency.
|
|
713
292
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*/
|
|
724
|
-
/**
|
|
725
|
-
* Read-only view of every namespace a routing/condition predicate can branch
|
|
726
|
-
* on. `user`/`responses`/`data` are the writable namespaces; `context` is the
|
|
727
|
-
* read-only computed one ({@link FunnelContext}).
|
|
728
|
-
*/
|
|
729
|
-
interface FunnelSnapshot {
|
|
730
|
-
user: Record<string, VariableValue>;
|
|
731
|
-
responses: Record<string, VariableValue>;
|
|
732
|
-
data: Record<string, VariableValue>;
|
|
733
|
-
context: FunnelContext;
|
|
734
|
-
}
|
|
735
|
-
/** A code-predicate gate — `s => s.responses.goal === 'gain'`. */
|
|
736
|
-
type Predicate = (s: FunnelSnapshot) => boolean;
|
|
737
|
-
type ConditionOp = 'eq' | 'neq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' | 'exists' | 'empty' | 'contains';
|
|
738
|
-
/**
|
|
739
|
-
* A declarative gate over **one** namespaced field — UI-editable data the
|
|
740
|
-
* dashboard renders as a condition row (`field` `op` `value`). `field` is a
|
|
741
|
-
* dotted path into the snapshot: `responses.goal`, `user.country`,
|
|
742
|
-
* `context.device.isMobile`.
|
|
743
|
-
*/
|
|
744
|
-
interface Condition {
|
|
745
|
-
field: string;
|
|
746
|
-
op: ConditionOp;
|
|
747
|
-
value?: VariableValue | VariableValue[];
|
|
748
|
-
}
|
|
749
|
-
/** An edge gate: either declarative data ({@link Condition}) or a {@link Predicate}. */
|
|
750
|
-
type Gate = Condition | Predicate;
|
|
751
|
-
/**
|
|
752
|
-
* One routing edge out of a page. `to` is a **static** target key (the dashboard
|
|
753
|
-
* reads these to draw the flow graph); `when` gates the edge (omit = fallback).
|
|
754
|
-
*/
|
|
755
|
-
interface Route {
|
|
756
|
-
to: string;
|
|
757
|
-
when?: Gate;
|
|
758
|
-
}
|
|
759
|
-
/** Evaluate a declarative {@link Condition} against a snapshot. */
|
|
760
|
-
declare function evaluateCondition(cond: Condition, s: FunnelSnapshot): boolean;
|
|
761
|
-
/** Evaluate either gate kind to a boolean. */
|
|
762
|
-
declare function evaluateGate(gate: Gate, s: FunnelSnapshot): boolean;
|
|
763
|
-
/**
|
|
764
|
-
* Resolve an ordered list of routes to a target page key, or `undefined` to fall
|
|
765
|
-
* through to the linear next. First route whose gate passes (or has no `when`)
|
|
766
|
-
* wins.
|
|
767
|
-
*/
|
|
768
|
-
declare function resolveRoute(routes: Route[] | undefined, s: FunnelSnapshot): string | undefined;
|
|
769
|
-
type PageType = 'default' | 'email-capture' | 'paywall' | 'upsell' | 'finish';
|
|
770
|
-
interface PageMeta {
|
|
771
|
-
/** Page kind — drives behavior + analytics (e.g. `paywall`, `upsell`). */
|
|
772
|
-
type?: PageType;
|
|
773
|
-
/** URL slug; defaults to the page key. */
|
|
774
|
-
slug?: string;
|
|
775
|
-
/**
|
|
776
|
-
* Ordered routing edges out of this page (first match wins). Each has a static
|
|
777
|
-
* `to` key + an optional `when` gate. Omit for the default linear flow.
|
|
778
|
-
*/
|
|
779
|
-
next?: Route[];
|
|
780
|
-
/**
|
|
781
|
-
* Entry precondition for **deep-link / reload** restoration. When the runtime is
|
|
782
|
-
* asked to start *on this page* (not via in-funnel navigation), it restores there
|
|
783
|
-
* only if `guard` passes against the restored snapshot — otherwise it falls back
|
|
784
|
-
* to the start page. e.g. an upsell page: `guard: s => s.data.purchased === true`.
|
|
785
|
-
* Reaching the page via `next()` is unaffected (routing already gated it). Omit =
|
|
786
|
-
* always restorable. Same gate kind as routing (`Condition` or predicate).
|
|
787
|
-
*/
|
|
788
|
-
guard?: Gate;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Identity helper for a page's co-located metadata.
|
|
792
|
-
* `export const meta = pageMeta({ type: 'paywall', next: s => … })`.
|
|
793
|
-
*/
|
|
794
|
-
declare function pageMeta(meta: PageMeta): PageMeta;
|
|
795
|
-
/**
|
|
796
|
-
* The effective entry guard for a page: an explicit `meta.guard` if the author set
|
|
797
|
-
* one, **otherwise** the page-type default (e.g. paywall/upsell need email). The
|
|
798
|
-
* explicit guard fully **overrides** the default — it's not combined. Returns
|
|
799
|
-
* `undefined` when neither applies.
|
|
800
|
-
*/
|
|
801
|
-
declare function entryGuard(meta?: PageMeta): Gate | undefined;
|
|
802
|
-
/**
|
|
803
|
-
* Identity helper for a page component. The page is plain React that reaches the
|
|
804
|
-
* runtime through hooks (`useResponse`, `useNavigation`, …) — there is no `sdk`
|
|
805
|
-
* prop. `export default definePage(function Welcome() { … })`.
|
|
806
|
-
*/
|
|
807
|
-
declare function definePage<P extends object = Record<string, never>>(component: ComponentType<P>): ComponentType<P>;
|
|
808
|
-
/** A page in the flow: its key + (optional) co-located meta. */
|
|
809
|
-
interface FlowPage {
|
|
810
|
-
key: string;
|
|
811
|
-
meta?: PageMeta;
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* Decide the next page from `currentKey`:
|
|
815
|
-
* 1. the current page's `next` routes ({@link resolveRoute}), if one matches, else
|
|
816
|
-
* 2. the next page in file order (the default linear flow), else
|
|
817
|
-
* 3. `undefined` (end of funnel / current key not found).
|
|
818
|
-
*/
|
|
819
|
-
declare function nextPage(pages: FlowPage[], currentKey: string, s: FunnelSnapshot): string | undefined;
|
|
820
|
-
/**
|
|
821
|
-
* Every page a visitor *could* go to next from `currentKey` — all route targets
|
|
822
|
-
* (regardless of gate, since we don't know which will pass) plus the linear next.
|
|
823
|
-
* Used to **prefetch** the likely-next page chunks; not for navigation.
|
|
824
|
-
*/
|
|
825
|
-
declare function outgoingKeys(pages: FlowPage[], currentKey: string): string[];
|
|
826
|
-
/**
|
|
827
|
-
* The number of pages on the path from `startKey` to a `finish` (or the end of
|
|
828
|
-
* the flow) **given the current state** — the denominator for progress. It walks
|
|
829
|
-
* the actual routing ({@link nextPage}) rather than counting all files, so a
|
|
830
|
-
* branched funnel reaches 100% on whichever path the visitor's answers select.
|
|
831
|
-
* Re-traced per navigation; a visited set guards against routing cycles.
|
|
832
|
-
*/
|
|
833
|
-
declare function expectedPathLength(pages: FlowPage[], startKey: string, s: FunnelSnapshot): number;
|
|
834
|
-
interface FunnelLocales {
|
|
835
|
-
default: string;
|
|
836
|
-
supported: string[];
|
|
837
|
-
fallback?: string;
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* The funnel-level spine. Flow is mostly inferred from page file order + each
|
|
841
|
-
* page's `next`; this holds the funnel-wide config. Stays **thin** — routing
|
|
842
|
-
* lives co-located on pages (doc 06: "funnel.ts stays thin").
|
|
293
|
+
* Our only job is **display**: pick the offering by logical id and **format** its
|
|
294
|
+
* amount. Formatting happens at read time in the funnel's **active locale** (so
|
|
295
|
+
* switching language reformats `kr 79,00` ⇄ `$9.99`), while the **currency** is
|
|
296
|
+
* whatever the provider resolved. Currency selection is the platform's; the
|
|
297
|
+
* authoring funnel only ever names an offering id.
|
|
298
|
+
*
|
|
299
|
+
* The math itself (resolve/format/exponents) is pure and lives in {@link ./money}
|
|
300
|
+
* so the server/tooling entry can use it without React — this module re-exports
|
|
301
|
+
* it and adds the React wiring (provider + hooks).
|
|
843
302
|
*/
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
data?: Record<string, VariableConfig>;
|
|
850
|
-
/** Product ids from the platform catalog this funnel uses (prices resolved at render). */
|
|
851
|
-
products?: string[];
|
|
852
|
-
locales?: FunnelLocales;
|
|
853
|
-
/**
|
|
854
|
-
* When `true`, the funnel shows the **geo-specific** currency/price the payment
|
|
855
|
-
* provider resolves (Stripe `currency_options`, Paddle preview, Whop, SolidGate,
|
|
856
|
-
* …). When `false` (default), a single fixed currency is used. AppFunnel does no
|
|
857
|
-
* FX — this only toggles whether the platform asks the provider for the geo
|
|
858
|
-
* price; the SDK just displays whatever resolved `{amount, currency}` it's handed.
|
|
859
|
-
* (See docs/platform-v2 phase-3 §3.6.)
|
|
860
|
-
*/
|
|
861
|
-
locationAwareCurrency?: boolean;
|
|
862
|
-
}
|
|
863
|
-
/** Identity helper for a funnel definition. */
|
|
864
|
-
declare function defineFunnel(def: FunnelDefinition): FunnelDefinition;
|
|
303
|
+
|
|
304
|
+
/** `useOffering('monthly')` — one offering, formatted in the active locale. */
|
|
305
|
+
declare function useOffering(id: string | undefined): Offering | undefined;
|
|
306
|
+
/** `useOfferings()` — every offering, formatted in the active locale, in order. */
|
|
307
|
+
declare function useOfferings(): Offering[];
|
|
865
308
|
|
|
866
309
|
/**
|
|
867
310
|
* v2 localization — translated **text** (prices already localize via `Intl` in
|
|
@@ -881,16 +324,6 @@ declare function defineFunnel(def: FunnelDefinition): FunnelDefinition;
|
|
|
881
324
|
/** locale → (message key → string). The shape of `messages/<locale>.json`. */
|
|
882
325
|
type MessageCatalog = Record<string, Record<string, string>>;
|
|
883
326
|
type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
884
|
-
type Direction = 'ltr' | 'rtl';
|
|
885
|
-
/** Whether a locale is right-to-left (by language subtag). */
|
|
886
|
-
declare function isRtl(locale: string): boolean;
|
|
887
|
-
/**
|
|
888
|
-
* Resolve the active locale: `override` (URL/param) → `detected` (device) →
|
|
889
|
-
* funnel default, each only if supported (matched by exact tag or language
|
|
890
|
-
* subtag). Mirrors doc 05's resolution chain (the geo/header steps happen
|
|
891
|
-
* server-side and arrive via `override`/`detected`).
|
|
892
|
-
*/
|
|
893
|
-
declare function resolveLocale(config: FunnelLocales | undefined, detected: string, override?: string): string;
|
|
894
327
|
interface TFunction {
|
|
895
328
|
/** Translate a key, with optional `{{var}}` params. Missing → fallback → key. */
|
|
896
329
|
(key: string, params?: Record<string, string | number>): string;
|
|
@@ -914,6 +347,9 @@ interface Translation {
|
|
|
914
347
|
/** The funnel's supported locales. */
|
|
915
348
|
locales: string[];
|
|
916
349
|
}
|
|
350
|
+
/** The active (resolved) funnel locale, or `'en'` if no provider — for display
|
|
351
|
+
* formatting (e.g. {@link ./catalog} prices) without building the full handle. */
|
|
352
|
+
declare function useActiveLocale(): string;
|
|
917
353
|
/**
|
|
918
354
|
* `const { t, fmt, locale, dir } = useTranslation()` — translated text +
|
|
919
355
|
* locale-aware `Intl` formatters for the active funnel locale.
|
|
@@ -921,161 +357,22 @@ interface Translation {
|
|
|
921
357
|
declare function useTranslation(): Translation;
|
|
922
358
|
|
|
923
359
|
/**
|
|
924
|
-
* Toast — a thin convenience
|
|
360
|
+
* Toast — a thin convenience over {@link https://sonner.emilkowal.ski | sonner}.
|
|
925
361
|
*
|
|
926
362
|
* Authors call `toast.success(...)` / `toast.error(...)` anywhere in their funnel.
|
|
927
363
|
* `FunnelProvider` auto-mounts {@link FunnelToaster} (opt out with `toaster={false}`),
|
|
928
364
|
* so toasts work with no setup; render it yourself if you want a custom location.
|
|
929
365
|
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*/
|
|
934
|
-
|
|
935
|
-
/** Sonner's `<Toaster>` with funnel defaults (top-center); pass any `ToasterProps` to override. */
|
|
936
|
-
declare function FunnelToaster(props?: ToasterProps): ReactNode;
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* **Runtime page-level experiments** — A/B/n on a *single page inside one funnel*,
|
|
940
|
-
* resolved at render time (phase-4). No funnel duplication: a slot (e.g. the page
|
|
941
|
-
* `welcome`) has alternate versions filed as siblings (`welcome@b.tsx`), and the
|
|
942
|
-
* runtime shows **one** of them per visitor, collapsing the losers out of the
|
|
943
|
-
* linear flow. The flow keeps stable **slot keys** (the control's key); only the
|
|
944
|
-
* rendered component swaps — so routing, the manifest, and analytics all key on the
|
|
945
|
-
* slot, not the variant.
|
|
946
|
-
*
|
|
947
|
-
* **Where the wiring lives:** the *variant pages* are code (compiled into the
|
|
948
|
-
* bundle). The *experiment wiring* — slot, variant→page map, weights, status — is
|
|
949
|
-
* **operational data the platform owns** (a DB record, edited by the dashboard,
|
|
950
|
-
* changed without recompiling the funnel). The SDK is storage-agnostic: the
|
|
951
|
-
* platform hands the runtime experiment records to `FunnelProvider`, the same way
|
|
952
|
-
* it hands in resolved product prices. This module just resolves them.
|
|
953
|
-
*
|
|
954
|
-
* **The only source-side marker** is the `@` in a variant file's key
|
|
955
|
-
* (`welcome@b`): it tells the *build* (manifest, flow graph) that a page is an
|
|
956
|
-
* off-flow variant of its slot, so the structure is correct without the DB. The
|
|
957
|
-
* label/weight/experiment-id are not in source.
|
|
958
|
-
*
|
|
959
|
-
* Bucketing is a deterministic FNV-1a hash, **namespaced by experiment id** so
|
|
960
|
-
* independent experiments bucket orthogonally (the basis for layering 100+ tests,
|
|
961
|
-
* §4.3). The seed is the durable identity ({@link bucketingSeed}) — never the
|
|
962
|
-
* session/fingerprint — so a returning visitor keeps their variant (landmine #4).
|
|
963
|
-
* Stability across an *add-a-variant* edit is the platform's job: an experiment's
|
|
964
|
-
* variant set is immutable while running (reweighting / adding an arm = a new
|
|
965
|
-
* experiment version), so within one record the assignment never moves.
|
|
966
|
-
*
|
|
967
|
-
* This module is pure (no React, no I/O) → it unit-tests trivially and the
|
|
968
|
-
* dashboard/server can reuse the same assignment math.
|
|
366
|
+
* **Dependency policy:** sonner is a direct dependency, imported statically (it's a small
|
|
367
|
+
* lib that ships with every funnel — no lazy chunk, no forwarding proxy). `toaster={false}`
|
|
368
|
+
* still renders no toaster markup; a funnel that never toasts just never calls `toast`.
|
|
969
369
|
*/
|
|
970
370
|
|
|
971
371
|
/**
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
* kept because it's sound — only the *seed* changes (identity, not session).
|
|
975
|
-
*/
|
|
976
|
-
declare function fnv1a(input: string): number;
|
|
977
|
-
/** Map a seed string to a stable float in `[0, 1)`. */
|
|
978
|
-
declare function hashToUnit(seed: string): number;
|
|
979
|
-
/**
|
|
980
|
-
* Pick a variant key by weight from a stable unit position `u ∈ [0,1)`. Variants
|
|
981
|
-
* are walked in declaration order and the first whose cumulative weight band
|
|
982
|
-
* contains `u` wins, so the same `u` always lands in the same variant.
|
|
983
|
-
*/
|
|
984
|
-
declare function pickByWeight(weights: Record<string, number>, u: number): string;
|
|
985
|
-
/**
|
|
986
|
-
* Deterministically assign a visitor (`seed`) to a variant of one experiment.
|
|
987
|
-
* The hash is **namespaced by `experimentId`** so two experiments on the same
|
|
988
|
-
* traffic don't correlate (orthogonal layering).
|
|
372
|
+
* Sonner's `<Toaster>` with funnel defaults (top-center); pass any `ToasterProps` to override.
|
|
373
|
+
* SSR-safe — sonner renders an empty container server-side and hydrates cleanly on the client.
|
|
989
374
|
*/
|
|
990
|
-
declare function
|
|
991
|
-
/**
|
|
992
|
-
* The stable bucketing seed: the known `customerId`, else the signed `visitorId`.
|
|
993
|
-
* Never the session id or fingerprint (landmine #4). `null` when neither is known
|
|
994
|
-
* — the caller then serves control instead of bucketing on nothing.
|
|
995
|
-
*/
|
|
996
|
-
declare function bucketingSeed(context: FunnelContext): string | null;
|
|
997
|
-
/** Split a page key into its slot + optional variant: `welcome@b` → `{ slot, variant: 'b' }`. */
|
|
998
|
-
declare function parseSlotKey(key: string): {
|
|
999
|
-
slot: string;
|
|
1000
|
-
variant?: string;
|
|
1001
|
-
};
|
|
1002
|
-
/** True if a page key is an off-flow variant (`welcome@b`), by the source-side `@` marker. */
|
|
1003
|
-
declare function isVariantKey(key: string): boolean;
|
|
1004
|
-
/** One arm of an experiment: which page renders, at what weight. */
|
|
1005
|
-
interface ExperimentVariant {
|
|
1006
|
-
/** The page key this arm renders (the slot's control, or a `@variant` sibling). */
|
|
1007
|
-
page: string;
|
|
1008
|
-
/** Relative weight in the split (need not sum to 100; zero = paused arm). */
|
|
1009
|
-
weight: number;
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* A runtime experiment record — the operational wiring the **platform** owns and
|
|
1013
|
-
* hands to the SDK (not authored in the funnel source). The variant *pages* are
|
|
1014
|
-
* code in the bundle; this is the live config over them.
|
|
1015
|
-
*/
|
|
1016
|
-
interface RuntimeExperiment {
|
|
1017
|
-
id: string;
|
|
1018
|
-
/** The page key holding the flow position (the anchor / control). */
|
|
1019
|
-
slot: string;
|
|
1020
|
-
/** label → arm. One arm's `page` is the `slot` (control); others are `@variant` siblings. */
|
|
1021
|
-
variants: Record<string, ExperimentVariant>;
|
|
1022
|
-
/** Lifecycle. Absent = running. */
|
|
1023
|
-
status?: 'running' | 'paused' | 'stopped';
|
|
1024
|
-
/** When stopped, the variant label everyone sees until promotion bakes it into the slot. */
|
|
1025
|
-
winner?: string;
|
|
1026
|
-
/** Primary metric (analytics only; not used for resolution). */
|
|
1027
|
-
metric?: string;
|
|
1028
|
-
}
|
|
1029
|
-
interface ExperimentResolution {
|
|
1030
|
-
/** experiment id → assigned variant label. */
|
|
1031
|
-
assignments: Record<string, string>;
|
|
1032
|
-
/** Page keys forming the effective linear flow (variant siblings removed), in order. */
|
|
1033
|
-
activeKeys: string[];
|
|
1034
|
-
/** Slot key → the page key whose component should render there. */
|
|
1035
|
-
render: Record<string, string>;
|
|
1036
|
-
/** Variant page key → its slot key. For remapping a route that targets a variant. */
|
|
1037
|
-
slotOf: Record<string, string>;
|
|
1038
|
-
/** Slot key → the experiment id it hosts (drives the exposure event). */
|
|
1039
|
-
experimentOf: Record<string, string>;
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Resolve which page version each experiment slot shows for this visitor, and
|
|
1043
|
-
* which page keys remain in the linear flow.
|
|
1044
|
-
*
|
|
1045
|
-
* Variant pages are detected structurally by the `@` in their key, so they
|
|
1046
|
-
* collapse out of the linear flow even for a slot with no active experiment.
|
|
1047
|
-
* For each *running* experiment the visitor is bucketed (stable on `seed`); a
|
|
1048
|
-
* *stopped* one serves its `winner`; a *paused* one (or no seed) serves the slot.
|
|
1049
|
-
*/
|
|
1050
|
-
declare function resolveExperiments(pages: {
|
|
1051
|
-
key: string;
|
|
1052
|
-
}[], experiments: RuntimeExperiment[], seed: string | null): ExperimentResolution;
|
|
1053
|
-
/** One referential problem with an experiment record. */
|
|
1054
|
-
interface ExperimentIssue {
|
|
1055
|
-
/** The offending experiment id (`'*'` for a cross-experiment issue). */
|
|
1056
|
-
experimentId: string;
|
|
1057
|
-
code: 'slot_missing' | 'slot_is_variant' | 'slot_conflict' | 'duplicate_id' | 'variant_page_missing' | 'variant_slot_mismatch' | 'no_control' | 'no_traffic' | 'negative_weight' | 'bad_winner' | 'single_arm' | 'orphan_variant';
|
|
1058
|
-
message: string;
|
|
1059
|
-
}
|
|
1060
|
-
interface ExperimentValidation {
|
|
1061
|
-
ok: boolean;
|
|
1062
|
-
errors: ExperimentIssue[];
|
|
1063
|
-
warnings: ExperimentIssue[];
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Cross-check experiment records against the funnel's pages — the publish-time /
|
|
1067
|
-
* experiment-edit gate that catches the dangling references the runtime can't see
|
|
1068
|
-
* (the experiment wiring lives in the DB; the pages live in the build, so they can
|
|
1069
|
-
* drift). Pure: hand it the runtime experiments + the funnel's page keys (e.g.
|
|
1070
|
-
* `manifest.pages`).
|
|
1071
|
-
*
|
|
1072
|
-
* Errors block; warnings inform. `orphan_variant` is a warning, not an error — a
|
|
1073
|
-
* `@variant` file with no running experiment is dead-but-harmless (a paused test, or
|
|
1074
|
-
* one not wired yet).
|
|
1075
|
-
*/
|
|
1076
|
-
declare function validateExperiments(experiments: RuntimeExperiment[], pages: {
|
|
1077
|
-
key: string;
|
|
1078
|
-
}[]): ExperimentValidation;
|
|
375
|
+
declare function FunnelToaster(props?: ToasterProps): ReactNode;
|
|
1079
376
|
|
|
1080
377
|
/**
|
|
1081
378
|
* v2 namespace layer over the flat {@link VariableStore}.
|
|
@@ -1137,12 +434,21 @@ interface FunnelProviderProps {
|
|
|
1137
434
|
config?: FunnelDefinition;
|
|
1138
435
|
/** Server / session-restored values overriding declared defaults. */
|
|
1139
436
|
sessionValues?: Record<string, VariableValue>;
|
|
437
|
+
/**
|
|
438
|
+
* FLASH-FREE HYDRATION (SSR-isolation Option B): when true, the store is seeded with
|
|
439
|
+
* DEFAULTS only for the initial (hydration) render — matching the build-time-prerendered
|
|
440
|
+
* DEFAULT-state markup — and `sessionValues` are applied as a POST-hydration state update in
|
|
441
|
+
* a mount effect (`store.setMany`), so a returning visitor's saved answers pop into
|
|
442
|
+
* already-visible controls without an empty-shell flash or a hydration mismatch. Off (the
|
|
443
|
+
* default) preserves the original behavior: `sessionValues` seed the store at construction
|
|
444
|
+
* (used by SSR itself and by non-prerendered mounts). The client entry sets this to true. */
|
|
445
|
+
deferSessionValues?: boolean;
|
|
1140
446
|
/** The platform tracker; defaults to a silent console tracker (tests/dev). */
|
|
1141
447
|
tracker?: Tracker;
|
|
1142
448
|
/** Computed-context seed (funnel id, test/live mode, server-hydrated utm/session). */
|
|
1143
449
|
context?: Parameters<typeof buildContext>[0];
|
|
1144
|
-
/**
|
|
1145
|
-
|
|
450
|
+
/** Offerings with their provider-resolved price (`{ amount, currency }`). */
|
|
451
|
+
offerings?: OfferingInput[];
|
|
1146
452
|
/** Live page-level experiment records, platform/DB-sourced (not authored in funnel code). */
|
|
1147
453
|
experiments?: RuntimeExperiment[];
|
|
1148
454
|
/** The platform checkout driver; defaults to a mock that resolves successfully. */
|
|
@@ -1161,7 +467,7 @@ interface FunnelProviderProps {
|
|
|
1161
467
|
* Mounts a funnel: builds the writable {@link VariableStore} + the read-only
|
|
1162
468
|
* {@link FunnelContext} (once) and provides the runtime to the tree via hooks.
|
|
1163
469
|
*/
|
|
1164
|
-
declare function FunnelProvider({ config, sessionValues, tracker, context: contextOpts,
|
|
470
|
+
declare function FunnelProvider({ config, sessionValues, deferSessionValues, tracker, context: contextOpts, offerings, experiments, checkout, messages, locale: localeOverride, toaster, store, children, }: FunnelProviderProps): ReactNode;
|
|
1165
471
|
/**
|
|
1166
472
|
* The imperative handle. Does **not** subscribe — use it in event handlers and
|
|
1167
473
|
* effects (`useFunnel().responses.set({...})`). For reactive reads use the hooks.
|
|
@@ -1243,6 +549,14 @@ interface NavigationState {
|
|
|
1243
549
|
*/
|
|
1244
550
|
nextCandidates: string[];
|
|
1245
551
|
canGoBack: boolean;
|
|
552
|
+
/**
|
|
553
|
+
* True while a navigation's target page chunk is still resolving (a cold,
|
|
554
|
+
* un-prefetched lazy chunk). The current page stays on screen during this window
|
|
555
|
+
* (no Suspense-fallback flicker); read this to show a subtle "working" affordance
|
|
556
|
+
* so a tapped button doesn't feel dead. Usually false/instant when the next chunk
|
|
557
|
+
* was prefetched.
|
|
558
|
+
*/
|
|
559
|
+
isNavigating: boolean;
|
|
1246
560
|
}
|
|
1247
561
|
/**
|
|
1248
562
|
* `const variant = useExperiment('welcome-headline')` — the variant key this
|
|
@@ -1266,9 +580,20 @@ declare function useExperiment(id: string): string | undefined;
|
|
|
1266
580
|
*
|
|
1267
581
|
* Must live inside `<FunnelProvider>`.
|
|
1268
582
|
*/
|
|
1269
|
-
declare function FunnelView({ pages, initialKey, layout, fallback, prefetch: prefetchMode, onNavigate, }: {
|
|
583
|
+
declare function FunnelView({ pages, initialKey, trustInitialKey, layout, fallback, prefetch: prefetchMode, onNavigate, }: {
|
|
1270
584
|
pages: RuntimePage[];
|
|
1271
585
|
initialKey?: string;
|
|
586
|
+
/**
|
|
587
|
+
* FLASH-FREE HYDRATION (SSR-isolation Option B, FIX 1): honor `initialKey` VERBATIM as the entry
|
|
588
|
+
* page, WITHOUT re-evaluating its entry `guard` on the client. The SERVER already resolved the
|
|
589
|
+
* true entry — either by evaluating a declarative guard to pick the prerendered target-vs-bounce,
|
|
590
|
+
* or by running the real guard inside the fallback SSR render — and threaded the resolved key
|
|
591
|
+
* here. With `deferSessionValues` on, the client's initial snapshot is EMPTY (saved answers arrive
|
|
592
|
+
* post-hydration), so re-running the guard here would wrongly bounce a qualified visitor and
|
|
593
|
+
* mismatch the server markup. Set by the client entry (via createFunnelTree) alongside
|
|
594
|
+
* `deferSessionValues`; the guard still protects in-funnel navigation and non-deferred mounts.
|
|
595
|
+
*/
|
|
596
|
+
trustInitialKey?: boolean;
|
|
1272
597
|
layout?: ComponentType<{
|
|
1273
598
|
children: ReactNode;
|
|
1274
599
|
}>;
|
|
@@ -1294,6 +619,138 @@ declare function useNavigation(): NavigationState;
|
|
|
1294
619
|
/** `usePage()` — the current page's read-only context, reactive to navigation. */
|
|
1295
620
|
declare function usePage(): PageContext;
|
|
1296
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Funnel asset map: logical name (e.g. `logo.svg`) → content-addressed CDN URL,
|
|
624
|
+
* injected by the renderer. Assets live in a SHARED content-addressed store
|
|
625
|
+
* (`appfunnel/assets/<hash>`), so identical content is stored once across all
|
|
626
|
+
* builds and funnels while staying immutable. Both SSR and client read the same
|
|
627
|
+
* injected map → URLs match. Defaults to `{}` (no provider needed in tests).
|
|
628
|
+
*/
|
|
629
|
+
declare const AssetsContext: react.Context<Record<string, string>>;
|
|
630
|
+
/** Provides the funnel's asset map (mounted for you by {@link createFunnelTree}). */
|
|
631
|
+
declare function AssetsProvider({ assets, children, }: {
|
|
632
|
+
assets?: Record<string, string>;
|
|
633
|
+
children: ReactNode;
|
|
634
|
+
}): ReactNode;
|
|
635
|
+
/** The whole asset map (logical name → CDN URL). `{}` outside a provider. */
|
|
636
|
+
declare function useAssets(): Record<string, string>;
|
|
637
|
+
/** `useAsset('logo.svg')` — one asset's CDN URL, or `undefined` if not injected. */
|
|
638
|
+
declare function useAsset(name: string): string | undefined;
|
|
639
|
+
/** What the renderer injects when mounting a funnel build (SSR and hydration). */
|
|
640
|
+
interface MountOpts {
|
|
641
|
+
/** Platform-resolved offering prices (one currency per visitor). */
|
|
642
|
+
offerings?: OfferingInput[];
|
|
643
|
+
/** Explicit locale override (URL/param/geo); wins over the detected device locale. */
|
|
644
|
+
locale?: string;
|
|
645
|
+
mode?: 'test' | 'live';
|
|
646
|
+
/** Deep-link / reload entry page (honored only if its entry guard passes). */
|
|
647
|
+
initialKey?: string;
|
|
648
|
+
/** Asset map: logical name → content-addressed CDN URL. */
|
|
649
|
+
assets?: Record<string, string>;
|
|
650
|
+
/** Per-locale message catalogs (`messages/<locale>.json`) — overrides the mount's baked-in ones. */
|
|
651
|
+
messages?: MessageCatalog;
|
|
652
|
+
/** The SIGNED anonymous visitor id (bare id, verified server-side; phase-1 §1.2) — the
|
|
653
|
+
* renderer mints/reads the `af_vid` cookie and injects the value here. Feeds
|
|
654
|
+
* `context.identity.visitorId` (experiment bucketing fallback seed, session continuity). */
|
|
655
|
+
visitorId?: string | null;
|
|
656
|
+
/** Known customer id (identified visitor) — the preferred bucketing seed. */
|
|
657
|
+
customerId?: string | null;
|
|
658
|
+
/** Platform tracking config (phase-1 §1.4). Present → `createFunnelTree` wires the real
|
|
659
|
+
* platform tracker (events POST to the shared ingestion endpoint). Absent (preview, tests,
|
|
660
|
+
* bare mounts) → the console tracker. Serializable on purpose — it crosses the sealed-build
|
|
661
|
+
* boundary as data. */
|
|
662
|
+
tracking?: {
|
|
663
|
+
apiBase: string;
|
|
664
|
+
campaignId: string;
|
|
665
|
+
/** WebFunnel DB id (NOT the funnel.ts config id). */
|
|
666
|
+
funnelId: string;
|
|
667
|
+
/** FUNNEL-split assignment (phase-4) — the tracker emits its exposure event. */
|
|
668
|
+
experiment?: {
|
|
669
|
+
id: string;
|
|
670
|
+
variant: string;
|
|
671
|
+
version?: number;
|
|
672
|
+
};
|
|
673
|
+
/** Campaign ROUTE that served this first request (campaign-routing pivot) — the tracker forwards
|
|
674
|
+
* its id as `campaignRouteId` so ingestion stamps the session once at create. */
|
|
675
|
+
route?: {
|
|
676
|
+
id: string;
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
/** Server-hydrated namespace snapshot (phase-1 §1.1): flat namespaced keys
|
|
680
|
+
* (`user.email`, `responses.goal`, …) from the visitor's latest session — the renderer
|
|
681
|
+
* resolves it by visitorId so a returning visitor resumes with their answers. */
|
|
682
|
+
sessionValues?: Record<string, string | number | boolean | null | string[]>;
|
|
683
|
+
/** RUNNING page-experiment records (phase-4): platform-owned wiring over the bundle's
|
|
684
|
+
* `@variant` pages, injected at render time — no rebuild to start/stop/reweight. */
|
|
685
|
+
experiments?: RuntimeExperiment[];
|
|
686
|
+
/** The server-resolved session id backing `sessionValues` — seeds the tracker (fallback)
|
|
687
|
+
* and `context.session.id`. */
|
|
688
|
+
sessionId?: string | null;
|
|
689
|
+
/** Mount the cross-origin {@link EmbedBridge} — the Funnel Embed API (flow snapshot + goTo/goNext/
|
|
690
|
+
* goPrevious + screenshot, over the versioned wire protocol). Set ONLY by preview surfaces (the live
|
|
691
|
+
* editor + the sealed `/preview/<buildId>` build); a live prod funnel leaves it off so it can't be
|
|
692
|
+
* driven / can't leak its flow state to an embedder. */
|
|
693
|
+
embed?: boolean;
|
|
694
|
+
/** FLASH-FREE HYDRATION (SSR-isolation Option B): set by the CLIENT entry only. The initial
|
|
695
|
+
* render is seeded with DEFAULTS (matching the build-time default-state prerender it hydrates
|
|
696
|
+
* against), then `sessionValues` are applied post-hydration via `store.setMany` — a returning
|
|
697
|
+
* visitor's saved answers pop into already-visible controls, no empty flash, no mismatch. Unset
|
|
698
|
+
* on SSR (the server render must bake sessionValues into the markup, the original behavior). */
|
|
699
|
+
deferSessionValues?: boolean;
|
|
700
|
+
/** SSR-isolation Option B, FIX 1: render `initialKey` VERBATIM without evaluating its entry guard.
|
|
701
|
+
* Set by the build-time PRERENDER stage so a guarded page (paywall/upsell) prerenders its OWN
|
|
702
|
+
* default-state markup (the renderer decides target-vs-bounce at serve time by evaluating the
|
|
703
|
+
* guard against the visitor's session). Defaults to `deferSessionValues` (the client trusts the
|
|
704
|
+
* server-resolved key); the prerender sets it true explicitly while leaving sessionValues baked. */
|
|
705
|
+
trustInitialKey?: boolean;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* The shape a funnel build's `mount.tsx` module must export — the named,
|
|
709
|
+
* versioned contract between a build and the renderer/editor:
|
|
710
|
+
* `import type { MountModule } from '@appfunnel-dev/sdk'`.
|
|
711
|
+
*/
|
|
712
|
+
type MountModule = {
|
|
713
|
+
pages: RuntimePage[];
|
|
714
|
+
tree: (opts: MountOpts) => ReactNode;
|
|
715
|
+
};
|
|
716
|
+
interface CreateFunnelTreeInput {
|
|
717
|
+
/** The funnel's spine (`defineFunnel(...)`). */
|
|
718
|
+
config: FunnelDefinition;
|
|
719
|
+
/** Code-split pages in file order (the same array `mount.tsx` exports). */
|
|
720
|
+
pages: RuntimePage[];
|
|
721
|
+
/** Persistent chrome wrapped around the page area (mounted once). */
|
|
722
|
+
layout?: ComponentType<{
|
|
723
|
+
children: ReactNode;
|
|
724
|
+
}>;
|
|
725
|
+
/**
|
|
726
|
+
* The platform checkout driver factory — `mount.tsx` imports it from a driver
|
|
727
|
+
* subpath entry (`import { stripeCheckoutDriver } from '@appfunnel-dev/sdk/driver-stripe'`)
|
|
728
|
+
* and passes it here, so the provider choice stays in the SDK-owned authoring
|
|
729
|
+
* contract while the ctx (apiBase/campaign/funnel/mode/identity) stays
|
|
730
|
+
* renderer-injected. Called once per `tree()` mount, only when tracking config
|
|
731
|
+
* is present — without it there is no API to charge against (preview, tests,
|
|
732
|
+
* bare mounts) and the mock driver fallback stays in place. Test mode still
|
|
733
|
+
* constructs the real driver: the wire contract carries `mode`, and the server
|
|
734
|
+
* resolves the provider's TEST store from it (that's the quarantine, not the client).
|
|
735
|
+
*/
|
|
736
|
+
checkoutDriver?: (ctx: CheckoutDriverContext) => CheckoutDriver;
|
|
737
|
+
/**
|
|
738
|
+
* The funnel's message catalogs (`messages/<locale>.json`), keyed by locale. The generated
|
|
739
|
+
* `mount.tsx` imports the catalogs and passes them here so `t()` resolves at render (SSR + client);
|
|
740
|
+
* `MountOpts.messages` can still override per-request. Omit for funnels with no translations.
|
|
741
|
+
*/
|
|
742
|
+
messages?: MessageCatalog;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Build a funnel's `tree(opts)` — the standard provider stack over your pages.
|
|
746
|
+
*
|
|
747
|
+
* ```tsx
|
|
748
|
+
* export const pages: RuntimePage[] = [ ... ]
|
|
749
|
+
* export const tree = createFunnelTree({ config, pages, layout: Layout })
|
|
750
|
+
* ```
|
|
751
|
+
*/
|
|
752
|
+
declare function createFunnelTree({ config, pages, layout, checkoutDriver, messages, }: CreateFunnelTreeInput): (opts: MountOpts) => ReactNode;
|
|
753
|
+
|
|
1297
754
|
/**
|
|
1298
755
|
* The two-tier component model (doc 07 §"magic ≠ black box"): the magic lives in
|
|
1299
756
|
* headless hooks (`useField`, `useNavigation`); these styleable components are
|
|
@@ -1437,76 +894,6 @@ type ModalProps = OverlayCommon;
|
|
|
1437
894
|
/** A centered dialog. Auto-binds to its `defineModal` — `<Modal>…`. */
|
|
1438
895
|
declare function Modal(props: ModalProps): ReactNode;
|
|
1439
896
|
|
|
1440
|
-
/**
|
|
1441
|
-
* The **`window.appfunnel` event bus** — the client-side integration surface
|
|
1442
|
-
* (port of v0 `admin/lib/funnel/utils/appfunnelRuntime.ts`).
|
|
1443
|
-
*
|
|
1444
|
-
* Every taxonomy event the runtime emits is mirrored here, so on-page pixels and
|
|
1445
|
-
* custom scripts integrate by **subscribing** — `window.appfunnel.on('purchase.complete', …)`
|
|
1446
|
-
* — instead of the funnel hand-injecting pixel config per page. The pre-built pixel
|
|
1447
|
-
* loaders (Meta/TikTok/GTM/Clarity) are just subscribers; CAPI stays the server-side
|
|
1448
|
-
* default (this is the *client* sink of the same events). See doc 08 §3.
|
|
1449
|
-
*
|
|
1450
|
-
* Two deliberate properties vs. v0:
|
|
1451
|
-
* - **Replay history** (`events`): async pixels load *after* `funnel.start`/`page.view`
|
|
1452
|
-
* fire, so a late subscriber would miss them. The bus keeps a bounded history a
|
|
1453
|
-
* subscriber can replay (the GTM `dataLayer` trick) — no lost events.
|
|
1454
|
-
* - **Subscribe + read only.** The window surface exposes `on`/`off`/`events` + read
|
|
1455
|
-
* getters; it has **no `emit`** (third-party code can't forge events) and **no
|
|
1456
|
-
* mutating actions** (`setVariable`/navigation) — the funnel can't be driven through
|
|
1457
|
-
* it. (v0's full-control Custom-JS API is deferred pending the bus-control-scope
|
|
1458
|
-
* decision; doc 08 §3 + §4.)
|
|
1459
|
-
*/
|
|
1460
|
-
|
|
1461
|
-
/** Read accessors the bus exposes — wired to the funnel's store + context. */
|
|
1462
|
-
interface BusAccessors {
|
|
1463
|
-
getVariable: (key: string) => unknown;
|
|
1464
|
-
getVariables: () => Record<string, unknown>;
|
|
1465
|
-
getCurrentPageId: () => string | null;
|
|
1466
|
-
getCustomerId: () => string | null;
|
|
1467
|
-
getVisitorId: () => string | null;
|
|
1468
|
-
}
|
|
1469
|
-
interface BusEvent {
|
|
1470
|
-
event: string;
|
|
1471
|
-
data: unknown;
|
|
1472
|
-
/** Epoch ms the event was emitted (for ordering / replay). */
|
|
1473
|
-
ts: number;
|
|
1474
|
-
}
|
|
1475
|
-
type Listener = (data: unknown) => void;
|
|
1476
|
-
/** The public `window.appfunnel` surface — subscribe + read, never emit or mutate. */
|
|
1477
|
-
interface AppFunnelBusApi extends BusAccessors {
|
|
1478
|
-
/** Subscribe to an event (or `'*'` for all). Returns an unsubscribe fn. */
|
|
1479
|
-
on(event: string, cb: Listener): () => void;
|
|
1480
|
-
off(event: string, cb: Listener): void;
|
|
1481
|
-
/** Bounded replay history — read past events, then `on()` for future ones. */
|
|
1482
|
-
readonly events: BusEvent[];
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* The internal bus: the public {@link AppFunnelBusApi} plus `emit` (SDK-only) and
|
|
1486
|
-
* `now` injection. The SDK holds this; only {@link AppFunnelBusApi} reaches `window`.
|
|
1487
|
-
*/
|
|
1488
|
-
interface FunnelBus extends AppFunnelBusApi {
|
|
1489
|
-
emit(event: string, data?: unknown): void;
|
|
1490
|
-
}
|
|
1491
|
-
/** Create a bus over the given read accessors. `now` is injectable for tests. */
|
|
1492
|
-
declare function createBus(accessors: BusAccessors, now?: () => number): FunnelBus;
|
|
1493
|
-
/**
|
|
1494
|
-
* Wrap a {@link Tracker} so every emitted taxonomy event is mirrored to the bus
|
|
1495
|
-
* (the client sink). `identify()` also surfaces a `user.registered` to the bus, so
|
|
1496
|
-
* pixels see registration. The server sink (the inner tracker → CAPI) is untouched.
|
|
1497
|
-
*/
|
|
1498
|
-
declare function withBus(tracker: Tracker, bus: FunnelBus): Tracker;
|
|
1499
|
-
declare global {
|
|
1500
|
-
interface Window {
|
|
1501
|
-
appfunnel?: AppFunnelBusApi;
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Attach the bus's public surface to `window.appfunnel` (client-side only).
|
|
1506
|
-
* Returns a detach fn for unmount. No-op under SSR.
|
|
1507
|
-
*/
|
|
1508
|
-
declare function attachBus(bus: FunnelBus): () => void;
|
|
1509
|
-
|
|
1510
897
|
/**
|
|
1511
898
|
* The **event catalog** — the single source of truth for *which* events exist and
|
|
1512
899
|
* how each is routed (analytics vs. integrations vs. runtime-only). Pairs with the
|
|
@@ -1606,6 +993,63 @@ declare function isIntegrationEvent(name: string): boolean;
|
|
|
1606
993
|
/** The events that feed integrations — the set a pixel loader subscribes to. */
|
|
1607
994
|
declare function integrationEvents(): CatalogEvent[];
|
|
1608
995
|
|
|
996
|
+
/**
|
|
997
|
+
* The REAL platform tracker (phase-1 §1.4 / BREAKING-CHANGES B-1) — POSTs the taxonomy to
|
|
998
|
+
* `POST {apiBase}/campaign/{campaignId}/event`, the SAME ingestion endpoint v1 funnels use, so
|
|
999
|
+
* v2 sessions/customers/events land in the shared analytics graph with zero schema fork.
|
|
1000
|
+
*
|
|
1001
|
+
* Built INSIDE the sealed bundle from serializable config (`MountOpts.tracking`) — the renderer
|
|
1002
|
+
* can only pass data across the build boundary, never live objects. Wired automatically by
|
|
1003
|
+
* `createFunnelTree` when tracking config is present.
|
|
1004
|
+
*
|
|
1005
|
+
* Behavior:
|
|
1006
|
+
* - SSR-safe: on the server (or with no config) this is never constructed — `createFunnelTree`
|
|
1007
|
+
* only wires it client-side; the export also guards `window` for direct users.
|
|
1008
|
+
* - mode:'test' → console tracker (previews/QA never pollute shared analytics; the real
|
|
1009
|
+
* test-mode quarantine is phase-3 §3.1c).
|
|
1010
|
+
* - The FIRST event establishes the session: sends are SERIALIZED until the server returns a
|
|
1011
|
+
* `sessionId` (avoids N parallel first-events racing session creation), then fire-and-forget.
|
|
1012
|
+
* The sessionId is kept in sessionStorage (per-tab reload continuity) — durable cross-visit
|
|
1013
|
+
* identity is the `visitorId`'s job, not the session's.
|
|
1014
|
+
* - `visitorId` (bare id, SSR-injected from the af_vid cookie) rides on every event — the
|
|
1015
|
+
* ingestion side uses it for durable session lookup + anonymous-Customer continuity.
|
|
1016
|
+
* - Acquisition (utm + click-ids + fbp/fbc + referrer) is folded into the FIRST event's
|
|
1017
|
+
* `userData.query` bag — exactly where the v1 ingestion's `extractUtmParams`/attribution
|
|
1018
|
+
* upgrade logic already looks. Pushed once; never re-sent.
|
|
1019
|
+
* - Failures are swallowed (console.warn) — tracking must never break a funnel.
|
|
1020
|
+
*/
|
|
1021
|
+
|
|
1022
|
+
interface PlatformTrackingConfig {
|
|
1023
|
+
/** Public API origin, e.g. `https://api.appfunnel.net` (no trailing slash). */
|
|
1024
|
+
apiBase: string;
|
|
1025
|
+
/** Campaign DB id (the ingestion route key). */
|
|
1026
|
+
campaignId: string;
|
|
1027
|
+
/** WebFunnel DB id (NOT the funnel.ts config id). */
|
|
1028
|
+
funnelId: string;
|
|
1029
|
+
/** Verified bare visitor id (phase-1 §1.2), SSR-injected. */
|
|
1030
|
+
visitorId?: string | null;
|
|
1031
|
+
/** Server-resolved session id (renderer looked it up by visitorId for hydration) — a fallback
|
|
1032
|
+
* seed; a per-tab sessionStorage id wins over it (v1 tab semantics). */
|
|
1033
|
+
sessionId?: string | null;
|
|
1034
|
+
/** FUNNEL-split experiment assignment (phase-4): the renderer picked this funnel for the
|
|
1035
|
+
* visitor — the tracker emits the experiment.exposure (page-level exposures come from the
|
|
1036
|
+
* flow runtime instead). Deduped per (session, experiment) in analytics. */
|
|
1037
|
+
experiment?: {
|
|
1038
|
+
id: string;
|
|
1039
|
+
variant: string;
|
|
1040
|
+
version?: number;
|
|
1041
|
+
};
|
|
1042
|
+
/** Campaign ROUTE that served this first request (campaign-routing pivot): the renderer's
|
|
1043
|
+
* serve-time evaluator matched this route → its id rides every event as `campaignRouteId`, and
|
|
1044
|
+
* ingestion STAMPS it once at session create (WebFunnelSession.campaignRouteId — a snapshot, like
|
|
1045
|
+
* the experiment exposure). Absent for the default funnel / no route matched. */
|
|
1046
|
+
route?: {
|
|
1047
|
+
id: string;
|
|
1048
|
+
};
|
|
1049
|
+
mode?: 'test' | 'live';
|
|
1050
|
+
}
|
|
1051
|
+
declare function createPlatformTracker(cfg: PlatformTrackingConfig): Tracker;
|
|
1052
|
+
|
|
1609
1053
|
/**
|
|
1610
1054
|
* `<Script>` — the **declared third-party embed** primitive (phase-2 §2.3d).
|
|
1611
1055
|
*
|
|
@@ -1650,102 +1094,4 @@ interface ScriptProps {
|
|
|
1650
1094
|
*/
|
|
1651
1095
|
declare function Script({ src, strategy, id, async, defer, nonce, attributes, onLoad, onError, }: ScriptProps): null;
|
|
1652
1096
|
|
|
1653
|
-
|
|
1654
|
-
* The **manifest** — the declarative IR a funnel project compiles to (doc 06).
|
|
1655
|
-
* It's the machine-readable artifact the platform operates on without executing
|
|
1656
|
-
* the funnel: the dashboard draws the flow graph from it, analytics binds to its
|
|
1657
|
-
* **stable ids**, validation runs against it, and the renderer reads it to resolve
|
|
1658
|
-
* routing.
|
|
1659
|
-
*
|
|
1660
|
-
* `compileManifest` evaluates the spine (the funnel config + each page's co-located
|
|
1661
|
-
* `meta`) into that IR. It's pure — no I/O — so it unit-tests trivially. **It runs
|
|
1662
|
-
* server-side**, as the IR step of the publish build over the canonical files in our
|
|
1663
|
-
* storage (R2). The client/CLI never produces the authoritative manifest — R2 is the
|
|
1664
|
-
* source of truth, the server compiles. (A local dev preview may run it for itself,
|
|
1665
|
-
* but that output is never published.)
|
|
1666
|
-
*
|
|
1667
|
-
* Edge model mirrors the runtime ({@link ../flow/spine}.`nextPage`): a page's
|
|
1668
|
-
* `meta.next` routes become **branch** edges (with a serialized condition — a
|
|
1669
|
-
* declarative {@link Condition}, or an opaque marker for a code predicate), and the
|
|
1670
|
-
* implicit file-order fall-through becomes a **linear** edge (unless an
|
|
1671
|
-
* unconditional route already covers it, or the page is a `finish`).
|
|
1672
|
-
*
|
|
1673
|
-
* Experiments are **not** compiled here. A variant page is recognised structurally
|
|
1674
|
-
* by the `@` in its key (`welcome@b`) and tagged `variantOf` + collapsed out of the
|
|
1675
|
-
* linear flow; the experiment *wiring* (labels/weights/status) is operational data
|
|
1676
|
-
* the platform owns and overlays at render time, not part of the funnel build.
|
|
1677
|
-
*
|
|
1678
|
-
* Stable ids: the page **key** is the source anchor, so `id === key` here. The
|
|
1679
|
-
* durable analytics id is the semantic page name (a slot survives a winner being
|
|
1680
|
-
* promoted into it); transient `@variant` pages only ever key analytics by
|
|
1681
|
-
* `(experimentId, variant)` in immutable results — a platform concern.
|
|
1682
|
-
*/
|
|
1683
|
-
|
|
1684
|
-
interface ManifestPage {
|
|
1685
|
-
id: string;
|
|
1686
|
-
key: string;
|
|
1687
|
-
type: PageType;
|
|
1688
|
-
slug: string;
|
|
1689
|
-
index: number;
|
|
1690
|
-
/**
|
|
1691
|
-
* Set on a variant page (`welcome@b`) to its **slot** (`welcome`) — a structural
|
|
1692
|
-
* marker that this node is an off-flow alternate, not a linear step. The
|
|
1693
|
-
* experiment *wiring* (label/weight/id) is operational data the platform owns,
|
|
1694
|
-
* not the manifest.
|
|
1695
|
-
*/
|
|
1696
|
-
variantOf?: string;
|
|
1697
|
-
}
|
|
1698
|
-
type EdgeCondition = {
|
|
1699
|
-
kind: 'declarative';
|
|
1700
|
-
condition: Condition;
|
|
1701
|
-
}
|
|
1702
|
-
/** A code predicate — opaque body; `label` may be supplied later by AST parsing. */
|
|
1703
|
-
| {
|
|
1704
|
-
kind: 'predicate';
|
|
1705
|
-
label?: string;
|
|
1706
|
-
};
|
|
1707
|
-
interface ManifestEdge {
|
|
1708
|
-
from: string;
|
|
1709
|
-
to: string;
|
|
1710
|
-
kind: 'branch' | 'linear';
|
|
1711
|
-
/** Absent = unconditional (a fallback route, or the linear fall-through). */
|
|
1712
|
-
condition?: EdgeCondition;
|
|
1713
|
-
}
|
|
1714
|
-
interface ManifestValidation {
|
|
1715
|
-
ok: boolean;
|
|
1716
|
-
/** Page ids you can't proceed from (and that aren't a `finish`). */
|
|
1717
|
-
deadEnds: string[];
|
|
1718
|
-
/** Page ids not reachable from the start. */
|
|
1719
|
-
unreachable: string[];
|
|
1720
|
-
/** Routes whose target page key doesn't exist. */
|
|
1721
|
-
badTargets: {
|
|
1722
|
-
from: string;
|
|
1723
|
-
to: string;
|
|
1724
|
-
}[];
|
|
1725
|
-
/** True if the funnel never writes `user.email` (the mandatory identity step). */
|
|
1726
|
-
missingEmailCapture: boolean;
|
|
1727
|
-
}
|
|
1728
|
-
interface FunnelManifest {
|
|
1729
|
-
id: string;
|
|
1730
|
-
pages: ManifestPage[];
|
|
1731
|
-
flow: {
|
|
1732
|
-
start: string | null;
|
|
1733
|
-
nodes: string[];
|
|
1734
|
-
edges: ManifestEdge[];
|
|
1735
|
-
};
|
|
1736
|
-
products: string[];
|
|
1737
|
-
locales?: FunnelLocales;
|
|
1738
|
-
validation: ManifestValidation;
|
|
1739
|
-
}
|
|
1740
|
-
interface CompileInput {
|
|
1741
|
-
funnel: FunnelDefinition;
|
|
1742
|
-
/** Pages in file order (the default flow); `meta` holds type/slug/next. */
|
|
1743
|
-
pages: {
|
|
1744
|
-
key: string;
|
|
1745
|
-
meta?: PageMeta;
|
|
1746
|
-
}[];
|
|
1747
|
-
}
|
|
1748
|
-
/** Compile a funnel's spine into its declarative {@link FunnelManifest} IR. */
|
|
1749
|
-
declare function compileManifest(input: CompileInput): FunnelManifest;
|
|
1750
|
-
|
|
1751
|
-
export { type Acquisition, type AppFunnelBusApi, Back, type BackProps, type BrowserContext, type BusAccessors, type BusEvent, type CatalogEvent, Checkout, type CheckoutCallbacks, type CheckoutDriver, type CheckoutError, type CheckoutErrorCategory, type CheckoutHandle, type CheckoutIntent, type CheckoutProps, type CheckoutProvider, type CheckoutRequest, type CheckoutResult, type CheckoutStatus, type CheckoutSurface, Choice, type ChoiceOption, type ChoiceProps, type ClickIdContext, type CompileInput, type Condition, type ConditionOp, type DeviceContext, type Direction, EVENT_CATALOG, type EdgeCondition, type EventMeta, type EventSource, type ExperimentIssue, type ExperimentResolution, type ExperimentValidation, type ExperimentVariant, type FlowPage, type Formatters, type Funnel, type FunnelBus, type FunnelContext, type FunnelDefinition, type FunnelEventDataMap, type FunnelLocales, type FunnelManifest, FunnelProvider, type FunnelProviderProps, type FunnelSnapshot, FunnelToaster, FunnelView, type Gate, INLINE_SURFACES, type IdentityContext, type Interval, type KnownFunnelEvent, type LocaleContext, type ManifestEdge, type ManifestPage, type ManifestValidation, type MessageCatalog, type MockDriverOptions, Modal, type ModalHandler, type ModalHocProps, type ModalProps, type ModalState, type Money, Namespace, type NavigationState, Next, type NextProps, type OffSessionReliability, type OsContext, PROVIDER_PROFILES, type PageContext, type PageMeta, type PageModule, type PageType, type PluralCategory, type Predicate, type Product, type ProductInput, type ProviderProfile, type ResolvedProduct, type Route, type RuntimeExperiment, type RuntimePage, Script, type ScriptProps, type SessionContext, Sheet, type SheetProps, type SheetSide, type SurfaceCapability, type SystemContext, type TFunction, type Tracker, type Translation, Upsell, type UpsellKind, type UpsellProps, type UseCheckoutOptions, type UtmContext, type ValidationResult, VariableStore, assignVariant, attachBus, bucketingSeed, buildAcquisition, buildCatalog, checkoutError, compileManifest, createBus, createConsoleTracker, createFunnelStore, createMockDriver, defineFunnel, defineModal, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatProduct, hashToUnit, hideModal, integrationEvents, isInlineSurface, isIntegrationEvent, isMerchantOfRecord, isOrchestrator, isRtl, isTrackableEvent, isVariantKey, newEventId, nextPage, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, registerModal, removeModal, resolveExperiments, resolveLocale, resolveProduct, resolveRoute, showModal, surfacesFor, unregisterModal, useCheckout, useClickIds, useContextValue, useData, useDevice, useExperiment, useField, useFunnel, useLocale, useModal, useNavigation, usePage, useProduct, useProducts, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, validateCheckout, validateExperiments, validateUpsell, withBus };
|
|
1097
|
+
export { Acquisition, type AppfunnelBusApi, AssetsContext, AssetsProvider, Back, type BackProps, type BusAccessors, type BusEvent, type CatalogEvent, CheckoutDriver, CheckoutDriverContext, Choice, type ChoiceOption, type ChoiceProps, type CreateFunnelTreeInput, Direction, EVENT_CATALOG, type EventMeta, type EventSource, type Formatters, type Funnel, type FunnelBus, FunnelContext, FunnelDefinition, type FunnelEventDataMap, FunnelProvider, type FunnelProviderProps, FunnelSnapshot, FunnelToaster, FunnelView, type KnownFunnelEvent, type MessageCatalog, Modal, type ModalHandler, type ModalHocProps, type ModalProps, type ModalState, type MountModule, type MountOpts, Namespace, type NavigationState, Next, type NextProps, Offering, OfferingInput, PageContext, PageMeta, type PageModule, type PlatformTrackingConfig, type PluralCategory, RuntimeExperiment, type RuntimePage, Script, type ScriptProps, Sheet, type SheetProps, type SheetSide, type TFunction, type Tracker, type Translation, VariableStore, attachBus, buildContext, createBus, createConsoleTracker, createFunnelStore, createFunnelTree, createPlatformTracker, defineModal, hideModal, integrationEvents, isIntegrationEvent, isTrackableEvent, newEventId, registerModal, removeModal, showModal, unregisterModal, useActiveLocale, useAsset, useAssets, useClickIds, useContextValue, useData, useDevice, useExperiment, useField, useFunnel, useLocale, useModal, useNavigation, useOffering, useOfferings, usePage, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, withBus };
|