@appfunnel-dev/sdk 2.0.0-canary.0 → 2.0.0-canary.2
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/dist/capabilities-7_hy5f5G.d.cts +114 -0
- package/dist/capabilities-7_hy5f5G.d.ts +114 -0
- package/dist/checkout-CZmEvWfC.d.cts +317 -0
- package/dist/checkout-DiQvRT5q.d.ts +317 -0
- package/dist/chunk-7UC5VXOR.js +446 -0
- package/dist/chunk-7UC5VXOR.js.map +1 -0
- package/dist/chunk-LJYLGLFS.cjs +153 -0
- package/dist/chunk-LJYLGLFS.cjs.map +1 -0
- package/dist/chunk-UIR6TGEW.js +97 -0
- package/dist/chunk-UIR6TGEW.js.map +1 -0
- package/dist/chunk-VQOD2Z6Q.cjs +104 -0
- package/dist/chunk-VQOD2Z6Q.cjs.map +1 -0
- package/dist/chunk-YY375F2B.js +140 -0
- package/dist/chunk-YY375F2B.js.map +1 -0
- package/dist/chunk-Z3TWO2PW.cjs +475 -0
- package/dist/chunk-Z3TWO2PW.cjs.map +1 -0
- package/dist/driver-paddle.cjs +814 -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 +811 -0
- package/dist/driver-paddle.js.map +1 -0
- package/dist/driver-stripe.cjs +2253 -0
- package/dist/driver-stripe.cjs.map +1 -0
- package/dist/driver-stripe.d.cts +8 -0
- package/dist/driver-stripe.d.ts +8 -0
- package/dist/driver-stripe.js +2247 -0
- package/dist/driver-stripe.js.map +1 -0
- package/dist/index.cjs +1953 -813
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +183 -933
- package/dist/index.d.ts +183 -933
- package/dist/index.js +1674 -655
- package/dist/index.js.map +1 -1
- package/dist/manifest-DQThneiG.d.cts +777 -0
- package/dist/manifest-DQThneiG.d.ts +777 -0
- package/dist/manifest-entry.cjs +203 -0
- package/dist/manifest-entry.cjs.map +1 -0
- package/dist/manifest-entry.d.cts +184 -0
- package/dist/manifest-entry.d.ts +184 -0
- package/dist/manifest-entry.js +98 -0
- package/dist/manifest-entry.js.map +1 -0
- package/package.json +37 -4
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
type VariableType = 'string' | 'number' | 'boolean' | 'stringArray';
|
|
4
|
+
type VariableValue = string | number | boolean | string[] | null | undefined;
|
|
5
|
+
interface VariableConfig {
|
|
6
|
+
type: VariableType;
|
|
7
|
+
default?: VariableValue;
|
|
8
|
+
persist?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* v2 `context` — the **read-only, computed** namespace.
|
|
13
|
+
*
|
|
14
|
+
* Everything the funnel can *read* but never *write*: the device, the visitor's
|
|
15
|
+
* locale, the current page, acquisition params (UTM), the session, and system
|
|
16
|
+
* facts (funnel id, test/live mode, clock). It supersedes the v0 flat
|
|
17
|
+
* `page.*`/`device.*`/`browser.*`/`os.*`/`query.*`/`metadata.*` system variables
|
|
18
|
+
* (see {@link ./systemVariables}) by folding them into one nested object.
|
|
19
|
+
*
|
|
20
|
+
* Why it's *not* in the {@link ./variableStore}: the store holds **writable**
|
|
21
|
+
* state (`user`/`responses`/`data`) that persists + re-renders per key. Context
|
|
22
|
+
* is derived and read-only, so it lives as a plain object on a React context —
|
|
23
|
+
* no subscriptions, no churn. The one moving part (`page`) is merged into the
|
|
24
|
+
* snapshot from navigation state at read time.
|
|
25
|
+
*
|
|
26
|
+
* Authors read it through hooks (`useDevice()`, `useLocale()`, `usePage()`,
|
|
27
|
+
* `useUtm()`) or inside routing/condition predicates as `s.context.*`.
|
|
28
|
+
*/
|
|
29
|
+
interface DeviceContext {
|
|
30
|
+
type: 'mobile' | 'tablet' | 'desktop';
|
|
31
|
+
isMobile: boolean;
|
|
32
|
+
isTablet: boolean;
|
|
33
|
+
screenWidth: number;
|
|
34
|
+
screenHeight: number;
|
|
35
|
+
viewportWidth: number;
|
|
36
|
+
viewportHeight: number;
|
|
37
|
+
colorDepth: number;
|
|
38
|
+
pixelRatio: number;
|
|
39
|
+
}
|
|
40
|
+
interface BrowserContext {
|
|
41
|
+
name: string;
|
|
42
|
+
version: string;
|
|
43
|
+
userAgent: string;
|
|
44
|
+
language: string;
|
|
45
|
+
cookieEnabled: boolean;
|
|
46
|
+
online: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface OsContext {
|
|
49
|
+
name: string;
|
|
50
|
+
timezone: string;
|
|
51
|
+
}
|
|
52
|
+
interface LocaleContext {
|
|
53
|
+
/** Full locale, e.g. `en-US`. */
|
|
54
|
+
locale: string;
|
|
55
|
+
/** Language subtag, e.g. `en`. */
|
|
56
|
+
language: string;
|
|
57
|
+
/** Region subtag, e.g. `US` (may be empty). */
|
|
58
|
+
region: string;
|
|
59
|
+
/** IANA timezone, e.g. `America/New_York`. */
|
|
60
|
+
timeZone: string;
|
|
61
|
+
}
|
|
62
|
+
interface PageContext {
|
|
63
|
+
/** Current page key. */
|
|
64
|
+
key: string;
|
|
65
|
+
/** Current page URL slug (`meta.slug`, defaults to the key) — what the renderer puts in the path. */
|
|
66
|
+
slug: string;
|
|
67
|
+
/** 0-based position in the visited history. */
|
|
68
|
+
index: number;
|
|
69
|
+
/** Total pages in the (expected) flow. */
|
|
70
|
+
total: number;
|
|
71
|
+
/** 0–100. */
|
|
72
|
+
progressPercentage: number;
|
|
73
|
+
/** Epoch ms the current page was entered. */
|
|
74
|
+
startedAt: number;
|
|
75
|
+
}
|
|
76
|
+
/** Acquisition / ad params captured on entry (the old `query.*` utm slice). */
|
|
77
|
+
interface UtmContext {
|
|
78
|
+
source?: string;
|
|
79
|
+
medium?: string;
|
|
80
|
+
campaign?: string;
|
|
81
|
+
content?: string;
|
|
82
|
+
term?: string;
|
|
83
|
+
[key: string]: string | undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Per-network ad **click-ids** captured from the entry URL — the matching signals
|
|
87
|
+
* the server CAPI needs. Named per network (not a generic bag) so the platform +
|
|
88
|
+
* each integration map them precisely; `[key]` catches any others.
|
|
89
|
+
*/
|
|
90
|
+
interface ClickIdContext {
|
|
91
|
+
/** Meta / Facebook */
|
|
92
|
+
fbclid?: string;
|
|
93
|
+
/** Google Ads (+ gbraid/wbraid for app↔web) */
|
|
94
|
+
gclid?: string;
|
|
95
|
+
gbraid?: string;
|
|
96
|
+
wbraid?: string;
|
|
97
|
+
/** Microsoft / Bing */
|
|
98
|
+
msclkid?: string;
|
|
99
|
+
/** TikTok */
|
|
100
|
+
ttclid?: string;
|
|
101
|
+
/** Twitter / X */
|
|
102
|
+
twclid?: string;
|
|
103
|
+
/** Snapchat */
|
|
104
|
+
sccid?: string;
|
|
105
|
+
/** LinkedIn */
|
|
106
|
+
li_fat_id?: string;
|
|
107
|
+
/** Pinterest */
|
|
108
|
+
epik?: string;
|
|
109
|
+
/** Reddit */
|
|
110
|
+
rdt_cid?: string;
|
|
111
|
+
[key: string]: string | undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The full acquisition snapshot captured once on entry — the funnel reads `utm` /
|
|
115
|
+
* `clickIds` off {@link FunnelContext}; the SDK forwards this whole thing to the
|
|
116
|
+
* tracker (`setAcquisition`) so the platform persists it **first-class**: per-visit
|
|
117
|
+
* on the session + **first-touch (write-once) on the Customer**, and the click-ids /
|
|
118
|
+
* `_fbp`/`_fbc` cookies feed CAPI. Not stored as a generic attribute/answer.
|
|
119
|
+
*/
|
|
120
|
+
interface Acquisition {
|
|
121
|
+
utm: UtmContext;
|
|
122
|
+
clickIds: ClickIdContext;
|
|
123
|
+
/** CAPI cookies (`_fbp`/`_fbc`) read from `document.cookie` (not funnel-readable). */
|
|
124
|
+
cookies: {
|
|
125
|
+
fbp?: string;
|
|
126
|
+
fbc?: string;
|
|
127
|
+
};
|
|
128
|
+
/** `document.referrer` at entry. */
|
|
129
|
+
referrer: string;
|
|
130
|
+
/** Entry URL path (the landing page). */
|
|
131
|
+
landingPath: string;
|
|
132
|
+
}
|
|
133
|
+
interface SessionContext {
|
|
134
|
+
/** Server session id; null until resolved. */
|
|
135
|
+
id: string | null;
|
|
136
|
+
/** Epoch ms the session began. */
|
|
137
|
+
startedAt: number;
|
|
138
|
+
/** `document.referrer` at entry. */
|
|
139
|
+
referrer: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Durable identity for **stable experiment bucketing** (phase-4 §4.2). The
|
|
143
|
+
* preferred seed is `customerId` once known; before identification it's the
|
|
144
|
+
* signed anonymous `visitorId` (P1). It is deliberately **not** the session id or
|
|
145
|
+
* fingerprint — a returning visitor (e.g. via winback) must keep their variant
|
|
146
|
+
* across sessions/devices (landmine #4). Both null = unidentifiable traffic, and
|
|
147
|
+
* the experiment layer falls back to control rather than bucket on nothing.
|
|
148
|
+
*/
|
|
149
|
+
interface IdentityContext {
|
|
150
|
+
/** Known customer id (post-identification). The preferred bucketing seed. */
|
|
151
|
+
customerId: string | null;
|
|
152
|
+
/** Signed anonymous visitor id. The fallback bucketing seed. */
|
|
153
|
+
visitorId: string | null;
|
|
154
|
+
}
|
|
155
|
+
interface SystemContext {
|
|
156
|
+
funnelId: string;
|
|
157
|
+
campaignId: string;
|
|
158
|
+
/** Render mode — `test` for preview/QA, `live` for public traffic. */
|
|
159
|
+
mode: 'test' | 'live';
|
|
160
|
+
/** Epoch ms captured at build time (stable per render, not a live clock). */
|
|
161
|
+
now: number;
|
|
162
|
+
}
|
|
163
|
+
/** The whole read-only computed namespace, as read in predicates: `s.context.*`. */
|
|
164
|
+
interface FunnelContext {
|
|
165
|
+
device: DeviceContext;
|
|
166
|
+
browser: BrowserContext;
|
|
167
|
+
os: OsContext;
|
|
168
|
+
locale: LocaleContext;
|
|
169
|
+
page: PageContext;
|
|
170
|
+
utm: UtmContext;
|
|
171
|
+
/** Per-network ad click-ids captured on entry (funnel-readable; also forwarded for CAPI). */
|
|
172
|
+
clickIds: ClickIdContext;
|
|
173
|
+
session: SessionContext;
|
|
174
|
+
identity: IdentityContext;
|
|
175
|
+
system: SystemContext;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Assemble the full {@link Acquisition} snapshot from the context + `document`
|
|
179
|
+
* (cookies, referrer, path) — captured once on entry, forwarded to the tracker for
|
|
180
|
+
* first-class persistence (session + first-touch Customer) + CAPI.
|
|
181
|
+
*/
|
|
182
|
+
declare function buildAcquisition(context: FunnelContext): Acquisition;
|
|
183
|
+
interface BuildContextOptions {
|
|
184
|
+
funnelId?: string;
|
|
185
|
+
campaignId?: string;
|
|
186
|
+
mode?: 'test' | 'live';
|
|
187
|
+
/** Stable build-time clock (avoids `Date.now()` churn in renders/tests). */
|
|
188
|
+
now?: number;
|
|
189
|
+
sessionId?: string | null;
|
|
190
|
+
/** Known customer id — the preferred stable bucketing seed (phase-4 §4.2). */
|
|
191
|
+
customerId?: string | null;
|
|
192
|
+
/** Signed anonymous visitor id — the fallback bucketing seed. */
|
|
193
|
+
visitorId?: string | null;
|
|
194
|
+
/** Overrides for any slice — used by the server to hydrate `utm`/`session`. */
|
|
195
|
+
overrides?: Partial<FunnelContext>;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build the read-only {@link FunnelContext} once, at mount. `page` starts at the
|
|
199
|
+
* first page and is replaced per navigation when assembling a snapshot.
|
|
200
|
+
*/
|
|
201
|
+
declare function buildContext(opts?: BuildContextOptions): FunnelContext;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Pure catalog math — no React, no I/O. Split out of {@link ./catalog} so the
|
|
205
|
+
* server/tooling entry (`@appfunnel-dev/sdk/manifest`) can compile/resolve
|
|
206
|
+
* products without pulling react-dom or any component code into its graph.
|
|
207
|
+
* {@link ./catalog} re-exports everything here and adds the React wiring.
|
|
208
|
+
*/
|
|
209
|
+
type Interval = 'day' | 'week' | 'month' | 'year' | 'one_time';
|
|
210
|
+
/** What the platform hands the funnel per product (a single provider-resolved price). */
|
|
211
|
+
interface ProductInput {
|
|
212
|
+
id: string;
|
|
213
|
+
name?: string;
|
|
214
|
+
displayName?: string;
|
|
215
|
+
/** Provider-resolved price in **minor units** (the provider chose the currency). */
|
|
216
|
+
amount: number;
|
|
217
|
+
/** Currency of `amount`, as the provider resolved it (e.g. `DKK`). */
|
|
218
|
+
currency: string;
|
|
219
|
+
interval?: Interval;
|
|
220
|
+
intervalCount?: number;
|
|
221
|
+
trialDays?: number;
|
|
222
|
+
/** Trial price in minor units (omit/0 = free trial). */
|
|
223
|
+
trialAmount?: number;
|
|
224
|
+
/** Settlement provider — opaque to the author, used by the checkout driver. */
|
|
225
|
+
provider?: string;
|
|
226
|
+
}
|
|
227
|
+
/** A money value with a locale-formatted string. */
|
|
228
|
+
interface Money {
|
|
229
|
+
/** Major units, e.g. `79`. */
|
|
230
|
+
amount: number;
|
|
231
|
+
/** Minor units, e.g. `7900`. */
|
|
232
|
+
minor: number;
|
|
233
|
+
currency: string;
|
|
234
|
+
/** Locale-formatted, e.g. `kr 79,00` / `€9,99` / `$9.99`. */
|
|
235
|
+
formatted: string;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The product with its amounts resolved (currency + period math), **before**
|
|
239
|
+
* locale formatting — what the catalog stores. `useProduct` formats it.
|
|
240
|
+
*/
|
|
241
|
+
interface ResolvedProduct {
|
|
242
|
+
id: string;
|
|
243
|
+
name: string;
|
|
244
|
+
displayName: string;
|
|
245
|
+
provider: string;
|
|
246
|
+
currency: string;
|
|
247
|
+
priceMinor: number;
|
|
248
|
+
perDayMinor: number;
|
|
249
|
+
perWeekMinor: number;
|
|
250
|
+
perMonthMinor: number;
|
|
251
|
+
perYearMinor: number;
|
|
252
|
+
period: string;
|
|
253
|
+
periodly: string;
|
|
254
|
+
hasTrial: boolean;
|
|
255
|
+
trialDays: number;
|
|
256
|
+
trialMinor: number;
|
|
257
|
+
}
|
|
258
|
+
/** Display-ready product the author reads (every amount locale-formatted). */
|
|
259
|
+
interface Product {
|
|
260
|
+
id: string;
|
|
261
|
+
name: string;
|
|
262
|
+
displayName: string;
|
|
263
|
+
provider: string;
|
|
264
|
+
price: Money;
|
|
265
|
+
/** e.g. `month`, `quarter`, `one-time`. */
|
|
266
|
+
period: string;
|
|
267
|
+
/** e.g. `monthly`, `quarterly`, `one-time`. */
|
|
268
|
+
periodly: string;
|
|
269
|
+
/** Period-normalized prices (for "just $0.27/day" style copy). */
|
|
270
|
+
perDay: Money;
|
|
271
|
+
perWeek: Money;
|
|
272
|
+
perMonth: Money;
|
|
273
|
+
perYear: Money;
|
|
274
|
+
hasTrial: boolean;
|
|
275
|
+
trialDays: number;
|
|
276
|
+
trialPrice: Money | null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolve a {@link ProductInput} into amounts + period math (no formatting yet).
|
|
280
|
+
* Per-period amounts are **integer minor units** (each derived from the exact
|
|
281
|
+
* daily rate, then rounded — never round-then-multiply, so error doesn't compound).
|
|
282
|
+
*/
|
|
283
|
+
declare function resolveProduct(input: ProductInput): ResolvedProduct;
|
|
284
|
+
/** Build the id → {@link ResolvedProduct} catalog. Formatting happens at read time. */
|
|
285
|
+
declare function buildCatalog(inputs: ProductInput[]): Map<string, ResolvedProduct>;
|
|
286
|
+
/**
|
|
287
|
+
* The minor-unit exponent for a currency (`USD`→2, `JPY`→0, `KWD`/`BHD`→3),
|
|
288
|
+
* derived from `Intl.NumberFormat(...).resolvedOptions().maximumFractionDigits`
|
|
289
|
+
* with a fallback of 2 for unknown/invalid codes.
|
|
290
|
+
*/
|
|
291
|
+
declare function currencyExponent(currency: string): number;
|
|
292
|
+
/** Format minor units of `currency` in `locale` (exponent-aware: 7900 JPY = ¥7,900). */
|
|
293
|
+
declare function formatMoney(minor: number, currency: string, locale: string): Money;
|
|
294
|
+
/** Format a {@link ResolvedProduct} into a display {@link Product} in `locale`. */
|
|
295
|
+
declare function formatProduct(r: ResolvedProduct, locale: string): Product;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The funnel **spine** — the constrained, declarative configuration authored as
|
|
299
|
+
* code (`defineFunnel` / `pageMeta` / `definePage`) yet **dashboard-editable**
|
|
300
|
+
* (static AST parse → pretty UI → codegen) and compiled to the manifest IR.
|
|
301
|
+
*
|
|
302
|
+
* Principle: **presentation = code** (the page components), **configuration =
|
|
303
|
+
* data** (this spine). See docs/platform-v2/06-funnel-project-model.md.
|
|
304
|
+
*
|
|
305
|
+
* Routing is **declarative nodes + code predicates** (decided — doc 06): a
|
|
306
|
+
* page's `next` is an ordered list of {@link Route}s, each with a **static `to`
|
|
307
|
+
* page key** (so the dashboard can draw every possible edge without executing
|
|
308
|
+
* anything) and an optional `when` **gate** that returns true/false. The gate is
|
|
309
|
+
* either a declarative {@link Condition} (one field — UI-editable data) or a code
|
|
310
|
+
* {@link Predicate} (`s => boolean` — the escape hatch, opaque body but the edge
|
|
311
|
+
* is still visible). First matching route wins; a route with no `when` is the
|
|
312
|
+
* fallback. Multiple conditions = multiple routes (or boolean logic in one
|
|
313
|
+
* predicate). Omit `next` entirely for the default linear flow.
|
|
314
|
+
*/
|
|
315
|
+
/**
|
|
316
|
+
* Read-only view of every namespace a routing/condition predicate can branch
|
|
317
|
+
* on. `user`/`responses`/`data` are the writable namespaces; `context` is the
|
|
318
|
+
* read-only computed one ({@link FunnelContext}).
|
|
319
|
+
*/
|
|
320
|
+
interface FunnelSnapshot {
|
|
321
|
+
user: Record<string, VariableValue>;
|
|
322
|
+
responses: Record<string, VariableValue>;
|
|
323
|
+
data: Record<string, VariableValue>;
|
|
324
|
+
context: FunnelContext;
|
|
325
|
+
}
|
|
326
|
+
/** A code-predicate gate — `s => s.responses.goal === 'gain'`. */
|
|
327
|
+
type Predicate = (s: FunnelSnapshot) => boolean;
|
|
328
|
+
type ConditionOp = 'eq' | 'neq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' | 'exists' | 'empty' | 'contains';
|
|
329
|
+
/**
|
|
330
|
+
* A declarative gate over **one** namespaced field — UI-editable data the
|
|
331
|
+
* dashboard renders as a condition row (`field` `op` `value`). `field` is a
|
|
332
|
+
* dotted path into the snapshot: `responses.goal`, `user.country`,
|
|
333
|
+
* `context.device.isMobile`.
|
|
334
|
+
*/
|
|
335
|
+
interface Condition {
|
|
336
|
+
field: string;
|
|
337
|
+
op: ConditionOp;
|
|
338
|
+
value?: VariableValue | VariableValue[];
|
|
339
|
+
}
|
|
340
|
+
/** An edge gate: either declarative data ({@link Condition}) or a {@link Predicate}. */
|
|
341
|
+
type Gate = Condition | Predicate;
|
|
342
|
+
/**
|
|
343
|
+
* One routing edge out of a page. `to` is a **static** target key (the dashboard
|
|
344
|
+
* reads these to draw the flow graph); `when` gates the edge (omit = fallback).
|
|
345
|
+
*/
|
|
346
|
+
interface Route {
|
|
347
|
+
to: string;
|
|
348
|
+
when?: Gate;
|
|
349
|
+
}
|
|
350
|
+
/** Evaluate a declarative {@link Condition} against a snapshot. */
|
|
351
|
+
declare function evaluateCondition(cond: Condition, s: FunnelSnapshot): boolean;
|
|
352
|
+
/** Evaluate either gate kind to a boolean. */
|
|
353
|
+
declare function evaluateGate(gate: Gate, s: FunnelSnapshot): boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Resolve an ordered list of routes to a target page key, or `undefined` to fall
|
|
356
|
+
* through to the linear next. First route whose gate passes (or has no `when`)
|
|
357
|
+
* wins.
|
|
358
|
+
*/
|
|
359
|
+
declare function resolveRoute(routes: Route[] | undefined, s: FunnelSnapshot): string | undefined;
|
|
360
|
+
type PageType = 'default' | 'email-capture' | 'paywall' | 'upsell' | 'finish';
|
|
361
|
+
interface PageMeta {
|
|
362
|
+
/** Page kind — drives behavior + analytics (e.g. `paywall`, `upsell`). */
|
|
363
|
+
type?: PageType;
|
|
364
|
+
/** URL slug; defaults to the page key. */
|
|
365
|
+
slug?: string;
|
|
366
|
+
/**
|
|
367
|
+
* Ordered routing edges out of this page (first match wins). Each has a static
|
|
368
|
+
* `to` key + an optional `when` gate. Omit for the default linear flow.
|
|
369
|
+
*/
|
|
370
|
+
next?: Route[];
|
|
371
|
+
/**
|
|
372
|
+
* Entry precondition for **deep-link / reload** restoration. When the runtime is
|
|
373
|
+
* asked to start *on this page* (not via in-funnel navigation), it restores there
|
|
374
|
+
* only if `guard` passes against the restored snapshot — otherwise it falls back
|
|
375
|
+
* to the start page. e.g. an upsell page: `guard: s => s.data.purchased === true`.
|
|
376
|
+
* Reaching the page via `next()` is unaffected (routing already gated it). Omit =
|
|
377
|
+
* always restorable. Same gate kind as routing (`Condition` or predicate).
|
|
378
|
+
*/
|
|
379
|
+
guard?: Gate;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Identity helper for a page's co-located metadata.
|
|
383
|
+
* `export const meta = pageMeta({ type: 'paywall', next: s => … })`.
|
|
384
|
+
*/
|
|
385
|
+
declare function pageMeta(meta: PageMeta): PageMeta;
|
|
386
|
+
/**
|
|
387
|
+
* The effective entry guard for a page: an explicit `meta.guard` if the author set
|
|
388
|
+
* one, **otherwise** the page-type default (e.g. paywall/upsell need email). The
|
|
389
|
+
* explicit guard fully **overrides** the default — it's not combined. Returns
|
|
390
|
+
* `undefined` when neither applies.
|
|
391
|
+
*/
|
|
392
|
+
declare function entryGuard(meta?: PageMeta): Gate | undefined;
|
|
393
|
+
/**
|
|
394
|
+
* Identity helper for a page component. The page is plain React that reaches the
|
|
395
|
+
* runtime through hooks (`useResponse`, `useNavigation`, …) — there is no `sdk`
|
|
396
|
+
* prop. `export default definePage(function Welcome() { … })`.
|
|
397
|
+
*/
|
|
398
|
+
declare function definePage<P extends object = Record<string, never>>(component: ComponentType<P>): ComponentType<P>;
|
|
399
|
+
/** A page in the flow: its key + (optional) co-located meta. */
|
|
400
|
+
interface FlowPage {
|
|
401
|
+
key: string;
|
|
402
|
+
meta?: PageMeta;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* A page as declared in `funnel.ts` — the key + its metadata, flattened. This is the
|
|
406
|
+
* AUTHORED shape (funnel.ts owns the page list, order, and routing); the build generates
|
|
407
|
+
* the code-split `mount.tsx` from it. Routing (`next`) must stay DECLARATIVE (Route[] with
|
|
408
|
+
* `{ to, when }` conditions, no predicate functions) so the web builder UI can edit it.
|
|
409
|
+
*/
|
|
410
|
+
interface FunnelPage extends PageMeta {
|
|
411
|
+
key: string;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Decide the next page from `currentKey`:
|
|
415
|
+
* 1. the current page's `next` routes ({@link resolveRoute}), if one matches, else
|
|
416
|
+
* 2. the next page in file order (the default linear flow), else
|
|
417
|
+
* 3. `undefined` (end of funnel / current key not found).
|
|
418
|
+
*/
|
|
419
|
+
declare function nextPage(pages: FlowPage[], currentKey: string, s: FunnelSnapshot): string | undefined;
|
|
420
|
+
/**
|
|
421
|
+
* Every page a visitor *could* go to next from `currentKey` — all route targets
|
|
422
|
+
* (regardless of gate, since we don't know which will pass) plus the linear next.
|
|
423
|
+
* Used to **prefetch** the likely-next page chunks; not for navigation.
|
|
424
|
+
*/
|
|
425
|
+
declare function outgoingKeys(pages: FlowPage[], currentKey: string): string[];
|
|
426
|
+
/**
|
|
427
|
+
* The number of pages on the path from `startKey` to a `finish` (or the end of
|
|
428
|
+
* the flow) **given the current state** — the denominator for progress. It walks
|
|
429
|
+
* the actual routing ({@link nextPage}) rather than counting all files, so a
|
|
430
|
+
* branched funnel reaches 100% on whichever path the visitor's answers select.
|
|
431
|
+
* Re-traced per navigation; a visited set guards against routing cycles.
|
|
432
|
+
*/
|
|
433
|
+
declare function expectedPathLength(pages: FlowPage[], startKey: string, s: FunnelSnapshot): number;
|
|
434
|
+
interface FunnelLocales {
|
|
435
|
+
default: string;
|
|
436
|
+
supported: string[];
|
|
437
|
+
fallback?: string;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The funnel-level spine. Flow is mostly inferred from page file order + each
|
|
441
|
+
* page's `next`; this holds the funnel-wide config. Stays **thin** — routing
|
|
442
|
+
* lives co-located on pages (doc 06: "funnel.ts stays thin").
|
|
443
|
+
*/
|
|
444
|
+
interface FunnelDefinition {
|
|
445
|
+
id: string;
|
|
446
|
+
/**
|
|
447
|
+
* The funnel's pages — order, type, and declarative routing — the source of truth the
|
|
448
|
+
* web builder edits and the build generates `mount.tsx` from. Each `key` maps to a
|
|
449
|
+
* `pages/<key>.tsx` component. Omit `pages` on hand-written funnels that still ship their
|
|
450
|
+
* own `mount.tsx` (back-compat); new/generated funnels declare them here.
|
|
451
|
+
*/
|
|
452
|
+
pages?: FunnelPage[];
|
|
453
|
+
/** Which checkout provider the paywall/checkout uses. The build wires the driver. */
|
|
454
|
+
checkout?: 'stripe' | 'paddle';
|
|
455
|
+
/** `responses.*` runtime variables (default values). */
|
|
456
|
+
responses?: Record<string, VariableConfig>;
|
|
457
|
+
/** `data.*` working/scratch variables (default values). */
|
|
458
|
+
data?: Record<string, VariableConfig>;
|
|
459
|
+
/** Product ids from the platform catalog this funnel uses (prices resolved at render). */
|
|
460
|
+
products?: string[];
|
|
461
|
+
locales?: FunnelLocales;
|
|
462
|
+
/**
|
|
463
|
+
* When `true`, the funnel shows the **geo-specific** currency/price the payment
|
|
464
|
+
* provider resolves (Stripe `currency_options`, Paddle preview, Whop, SolidGate,
|
|
465
|
+
* …). When `false` (default), a single fixed currency is used. AppFunnel does no
|
|
466
|
+
* FX — this only toggles whether the platform asks the provider for the geo
|
|
467
|
+
* price; the SDK just displays whatever resolved `{amount, currency}` it's handed.
|
|
468
|
+
* (See docs/platform-v2 phase-3 §3.6.)
|
|
469
|
+
*/
|
|
470
|
+
locationAwareCurrency?: boolean;
|
|
471
|
+
}
|
|
472
|
+
/** Identity helper for a funnel definition. */
|
|
473
|
+
declare function defineFunnel(def: FunnelDefinition): FunnelDefinition;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Pure locale resolution — no React. Split out of {@link ./translation} so the
|
|
477
|
+
* server/tooling entry (`@appfunnel-dev/sdk/manifest`) can resolve a visitor's
|
|
478
|
+
* locale without pulling component code. {@link ./translation} re-exports it.
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
type Direction = 'ltr' | 'rtl';
|
|
482
|
+
/** Whether a locale is right-to-left (by language subtag). */
|
|
483
|
+
declare function isRtl(locale: string): boolean;
|
|
484
|
+
/**
|
|
485
|
+
* Resolve the active locale: `override` (URL/param) → `detected` (device) →
|
|
486
|
+
* funnel default, each only if supported (matched by exact tag or language
|
|
487
|
+
* subtag). Mirrors doc 05's resolution chain (the geo/header steps happen
|
|
488
|
+
* server-side and arrive via `override`/`detected`).
|
|
489
|
+
*/
|
|
490
|
+
declare function resolveLocale(config: FunnelLocales | undefined, detected: string, override?: string): string;
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* **Runtime page-level experiments** — A/B/n on a *single page inside one funnel*,
|
|
494
|
+
* resolved at render time (phase-4). No funnel duplication: a slot (e.g. the page
|
|
495
|
+
* `welcome`) has alternate versions filed as siblings (`welcome@b.tsx`), and the
|
|
496
|
+
* runtime shows **one** of them per visitor, collapsing the losers out of the
|
|
497
|
+
* linear flow. The flow keeps stable **slot keys** (the control's key); only the
|
|
498
|
+
* rendered component swaps — so routing, the manifest, and analytics all key on the
|
|
499
|
+
* slot, not the variant.
|
|
500
|
+
*
|
|
501
|
+
* **Where the wiring lives:** the *variant pages* are code (compiled into the
|
|
502
|
+
* bundle). The *experiment wiring* — slot, variant→page map, weights, status — is
|
|
503
|
+
* **operational data the platform owns** (a DB record, edited by the dashboard,
|
|
504
|
+
* changed without recompiling the funnel). The SDK is storage-agnostic: the
|
|
505
|
+
* platform hands the runtime experiment records to `FunnelProvider`, the same way
|
|
506
|
+
* it hands in resolved product prices. This module just resolves them.
|
|
507
|
+
*
|
|
508
|
+
* **The only source-side marker** is the `@` in a variant file's key
|
|
509
|
+
* (`welcome@b`): it tells the *build* (manifest, flow graph) that a page is an
|
|
510
|
+
* off-flow variant of its slot, so the structure is correct without the DB. The
|
|
511
|
+
* label/weight/experiment-id are not in source.
|
|
512
|
+
*
|
|
513
|
+
* Bucketing is a deterministic FNV-1a hash, **namespaced by experiment id** so
|
|
514
|
+
* independent experiments bucket orthogonally (the basis for layering 100+ tests,
|
|
515
|
+
* §4.3). The seed is the durable identity ({@link bucketingSeed}) — never the
|
|
516
|
+
* session/fingerprint — so a returning visitor keeps their variant (landmine #4).
|
|
517
|
+
* Stability across an *add-a-variant* edit is the platform's job: an experiment's
|
|
518
|
+
* variant set is immutable while running (reweighting / adding an arm = a new
|
|
519
|
+
* experiment version), so within one record the assignment never moves.
|
|
520
|
+
*
|
|
521
|
+
* This module is pure (no React, no I/O) → it unit-tests trivially and the
|
|
522
|
+
* dashboard/server can reuse the same assignment math.
|
|
523
|
+
*/
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* FNV-1a 32-bit hash → unsigned int. Deterministic and isomorphic (no `crypto`,
|
|
527
|
+
* runs the same in the browser, a worker, and a build). The same math v0 used,
|
|
528
|
+
* kept because it's sound — only the *seed* changes (identity, not session).
|
|
529
|
+
*/
|
|
530
|
+
declare function fnv1a(input: string): number;
|
|
531
|
+
/** Map a seed string to a stable float in `[0, 1)`. */
|
|
532
|
+
declare function hashToUnit(seed: string): number;
|
|
533
|
+
/**
|
|
534
|
+
* Pick a variant key by weight from a stable unit position `u ∈ [0,1)`. Variants
|
|
535
|
+
* are walked in declaration order and the first whose cumulative weight band
|
|
536
|
+
* contains `u` wins, so the same `u` always lands in the same variant.
|
|
537
|
+
*/
|
|
538
|
+
declare function pickByWeight(weights: Record<string, number>, u: number): string;
|
|
539
|
+
/**
|
|
540
|
+
* Deterministically assign a visitor (`seed`) to a variant of one experiment.
|
|
541
|
+
* The hash is **namespaced by `experimentId`** so two experiments on the same
|
|
542
|
+
* traffic don't correlate (orthogonal layering).
|
|
543
|
+
*/
|
|
544
|
+
declare function assignVariant(experimentId: string, seed: string, weights: Record<string, number>): string;
|
|
545
|
+
/**
|
|
546
|
+
* The stable bucketing seed: the signed `visitorId`, else the known `customerId`.
|
|
547
|
+
* Never the session id or fingerprint (landmine #4). `null` when neither is known
|
|
548
|
+
* — the caller then serves control instead of bucketing on nothing.
|
|
549
|
+
*
|
|
550
|
+
* visitorId-FIRST (not customerId-first) is deliberate: the customerId injected for an
|
|
551
|
+
* anonymous visitor is minted server-side AFTER the first tracked event, so it is absent on
|
|
552
|
+
* visit 1 and present on visit 2 — seeding on it would flip a returning visitor's arm and
|
|
553
|
+
* count them in two arms of the same version. The signed `af_vid` cookie is the durable
|
|
554
|
+
* identity that's stable from the first pageload, so it seeds; customerId is only the fallback
|
|
555
|
+
* for cookieless SDK embeds. This matches the server-side FUNNEL split, which already seeds on
|
|
556
|
+
* the verified visitorId (resolve.ts). (Cross-device stable bucketing for IDENTIFIED customers
|
|
557
|
+
* is P3's IdentitySDK job, not this anonymous path.)
|
|
558
|
+
*/
|
|
559
|
+
declare function bucketingSeed(context: FunnelContext): string | null;
|
|
560
|
+
/** Split a page key into its slot + optional variant: `welcome@b` → `{ slot, variant: 'b' }`. */
|
|
561
|
+
declare function parseSlotKey(key: string): {
|
|
562
|
+
slot: string;
|
|
563
|
+
variant?: string;
|
|
564
|
+
};
|
|
565
|
+
/** True if a page key is an off-flow variant (`welcome@b`), by the source-side `@` marker. */
|
|
566
|
+
declare function isVariantKey(key: string): boolean;
|
|
567
|
+
/** One arm of an experiment: which page renders, at what weight. */
|
|
568
|
+
interface ExperimentVariant {
|
|
569
|
+
/** The page key this arm renders (the slot's control, or a `@variant` sibling). */
|
|
570
|
+
page: string;
|
|
571
|
+
/** Relative weight in the split (need not sum to 100; zero = paused arm). */
|
|
572
|
+
weight: number;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* A runtime experiment record — the operational wiring the **platform** owns and
|
|
576
|
+
* hands to the SDK (not authored in the funnel source). The variant *pages* are
|
|
577
|
+
* code in the bundle; this is the live config over them.
|
|
578
|
+
*/
|
|
579
|
+
interface RuntimeExperiment {
|
|
580
|
+
id: string;
|
|
581
|
+
/** The page key holding the flow position (the anchor / control). */
|
|
582
|
+
slot: string;
|
|
583
|
+
/** label → arm. One arm's `page` is the `slot` (control); others are `@variant` siblings. */
|
|
584
|
+
variants: Record<string, ExperimentVariant>;
|
|
585
|
+
/** Lifecycle. Absent = running. */
|
|
586
|
+
status?: 'running' | 'paused' | 'stopped';
|
|
587
|
+
/** When stopped, the variant label everyone sees until promotion bakes it into the slot. */
|
|
588
|
+
winner?: string;
|
|
589
|
+
/** Primary metric (analytics only; not used for resolution). */
|
|
590
|
+
metric?: string;
|
|
591
|
+
/** Platform record version (immutable-while-running; reweight = new version). Rides on
|
|
592
|
+
* exposure events so results key on (experimentId, version, variant). */
|
|
593
|
+
version?: number;
|
|
594
|
+
}
|
|
595
|
+
interface ExperimentResolution {
|
|
596
|
+
/** experiment id → assigned variant label. */
|
|
597
|
+
assignments: Record<string, string>;
|
|
598
|
+
/** Page keys forming the effective linear flow (variant siblings removed), in order. */
|
|
599
|
+
activeKeys: string[];
|
|
600
|
+
/** Slot key → the page key whose component should render there. */
|
|
601
|
+
render: Record<string, string>;
|
|
602
|
+
/** Variant page key → its slot key. For remapping a route that targets a variant. */
|
|
603
|
+
slotOf: Record<string, string>;
|
|
604
|
+
/** Slot key → the experiment id it hosts (drives the exposure event). */
|
|
605
|
+
experimentOf: Record<string, string>;
|
|
606
|
+
/** Experiment id → platform record version (rides on exposure events). */
|
|
607
|
+
versionOf: Record<string, number>;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Weights map (label → weight) for one experiment's arms. Exported (via the pure
|
|
611
|
+
* manifest entry) so the server-side FUNNEL split (renderer resolve) projects arms
|
|
612
|
+
* the same way before calling {@link assignVariant}, instead of re-inlining it.
|
|
613
|
+
*/
|
|
614
|
+
declare function weightsOf(variants: Record<string, {
|
|
615
|
+
weight: number;
|
|
616
|
+
}>): Record<string, number>;
|
|
617
|
+
/**
|
|
618
|
+
* Resolve which page version each experiment slot shows for this visitor, and
|
|
619
|
+
* which page keys remain in the linear flow.
|
|
620
|
+
*
|
|
621
|
+
* Variant pages are detected structurally by the `@` in their key, so they
|
|
622
|
+
* collapse out of the linear flow even for a slot with no active experiment.
|
|
623
|
+
* For each *running* experiment the visitor is bucketed (stable on `seed`); a
|
|
624
|
+
* *stopped* one serves its `winner`; a *paused* one (or no seed) serves the slot.
|
|
625
|
+
*/
|
|
626
|
+
declare function resolveExperiments(pages: {
|
|
627
|
+
key: string;
|
|
628
|
+
}[], experiments: RuntimeExperiment[], seed: string | null): ExperimentResolution;
|
|
629
|
+
/** One referential problem with an experiment record. */
|
|
630
|
+
interface ExperimentIssue {
|
|
631
|
+
/** The offending experiment id (`'*'` for a cross-experiment issue). */
|
|
632
|
+
experimentId: string;
|
|
633
|
+
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';
|
|
634
|
+
message: string;
|
|
635
|
+
}
|
|
636
|
+
interface ExperimentValidation {
|
|
637
|
+
ok: boolean;
|
|
638
|
+
errors: ExperimentIssue[];
|
|
639
|
+
warnings: ExperimentIssue[];
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Cross-check experiment records against the funnel's pages — the publish-time /
|
|
643
|
+
* experiment-edit gate that catches the dangling references the runtime can't see
|
|
644
|
+
* (the experiment wiring lives in the DB; the pages live in the build, so they can
|
|
645
|
+
* drift). Pure: hand it the runtime experiments + the funnel's page keys (e.g.
|
|
646
|
+
* `manifest.pages`).
|
|
647
|
+
*
|
|
648
|
+
* Errors block; warnings inform. `orphan_variant` is a warning, not an error — a
|
|
649
|
+
* `@variant` file with no running experiment is dead-but-harmless (a paused test, or
|
|
650
|
+
* one not wired yet).
|
|
651
|
+
*/
|
|
652
|
+
declare function validateExperiments(experiments: RuntimeExperiment[], pages: {
|
|
653
|
+
key: string;
|
|
654
|
+
}[]): ExperimentValidation;
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The **manifest** — the declarative IR a funnel project compiles to (doc 06).
|
|
658
|
+
* It's the machine-readable artifact the platform operates on without executing
|
|
659
|
+
* the funnel: the dashboard draws the flow graph from it, analytics binds to its
|
|
660
|
+
* **stable ids**, validation runs against it, and the renderer reads it to resolve
|
|
661
|
+
* routing.
|
|
662
|
+
*
|
|
663
|
+
* `compileManifest` evaluates the spine (the funnel config + each page's co-located
|
|
664
|
+
* `meta`) into that IR. It's pure — no I/O — so it unit-tests trivially. **It runs
|
|
665
|
+
* server-side**, as the IR step of the publish build over the canonical files in our
|
|
666
|
+
* storage (R2). The client/CLI never produces the authoritative manifest — R2 is the
|
|
667
|
+
* source of truth, the server compiles. (A local dev preview may run it for itself,
|
|
668
|
+
* but that output is never published.)
|
|
669
|
+
*
|
|
670
|
+
* Edge model mirrors the runtime ({@link ../flow/spine}.`nextPage`): a page's
|
|
671
|
+
* `meta.next` routes become **branch** edges (with a serialized condition — a
|
|
672
|
+
* declarative {@link Condition}, or an opaque marker for a code predicate), and the
|
|
673
|
+
* implicit file-order fall-through becomes a **linear** edge (unless an
|
|
674
|
+
* unconditional route already covers it, or the page is a `finish`).
|
|
675
|
+
*
|
|
676
|
+
* Experiments are **not** compiled here. A variant page is recognised structurally
|
|
677
|
+
* by the `@` in its key (`welcome@b`) and tagged `variantOf` + collapsed out of the
|
|
678
|
+
* linear flow; the experiment *wiring* (labels/weights/status) is operational data
|
|
679
|
+
* the platform owns and overlays at render time, not part of the funnel build.
|
|
680
|
+
*
|
|
681
|
+
* Stable ids: the page **key** is the source anchor, so `id === key` here. The
|
|
682
|
+
* durable analytics id is the semantic page name (a slot survives a winner being
|
|
683
|
+
* promoted into it); transient `@variant` pages only ever key analytics by
|
|
684
|
+
* `(experimentId, variant)` in immutable results — a platform concern.
|
|
685
|
+
*/
|
|
686
|
+
|
|
687
|
+
interface ManifestPage {
|
|
688
|
+
id: string;
|
|
689
|
+
key: string;
|
|
690
|
+
type: PageType;
|
|
691
|
+
slug: string;
|
|
692
|
+
index: number;
|
|
693
|
+
/**
|
|
694
|
+
* Set on a variant page (`welcome@b`) to its **slot** (`welcome`) — a structural
|
|
695
|
+
* marker that this node is an off-flow alternate, not a linear step. The
|
|
696
|
+
* experiment *wiring* (label/weight/id) is operational data the platform owns,
|
|
697
|
+
* not the manifest.
|
|
698
|
+
*/
|
|
699
|
+
variantOf?: string;
|
|
700
|
+
}
|
|
701
|
+
type EdgeCondition = {
|
|
702
|
+
kind: 'declarative';
|
|
703
|
+
condition: Condition;
|
|
704
|
+
}
|
|
705
|
+
/** A code predicate — opaque body; `label` may be supplied later by AST parsing. */
|
|
706
|
+
| {
|
|
707
|
+
kind: 'predicate';
|
|
708
|
+
label?: string;
|
|
709
|
+
};
|
|
710
|
+
interface ManifestEdge {
|
|
711
|
+
from: string;
|
|
712
|
+
to: string;
|
|
713
|
+
kind: 'branch' | 'linear';
|
|
714
|
+
/** Absent = unconditional (a fallback route, or the linear fall-through). */
|
|
715
|
+
condition?: EdgeCondition;
|
|
716
|
+
}
|
|
717
|
+
interface ManifestValidation {
|
|
718
|
+
/**
|
|
719
|
+
* `ok` = none of the hard errors: the original set (dead ends, unreachable
|
|
720
|
+
* pages, bad targets, missing email capture) AND the structural errors added
|
|
721
|
+
* later (duplicate keys, orphan variants, no reachable finish). Slug
|
|
722
|
+
* collisions are a **warning** — reported but not folded into `ok` (slugs are
|
|
723
|
+
* display/URL sugar; keys are the routing identity).
|
|
724
|
+
*/
|
|
725
|
+
ok: boolean;
|
|
726
|
+
/** Page ids you can't proceed from (and that aren't a `finish`). */
|
|
727
|
+
deadEnds: string[];
|
|
728
|
+
/** Page ids not reachable from the start. */
|
|
729
|
+
unreachable: string[];
|
|
730
|
+
/** Routes whose target page key doesn't exist. */
|
|
731
|
+
badTargets: {
|
|
732
|
+
from: string;
|
|
733
|
+
to: string;
|
|
734
|
+
}[];
|
|
735
|
+
/** True if the funnel never writes `user.email` (the mandatory identity step). */
|
|
736
|
+
missingEmailCapture: boolean;
|
|
737
|
+
/** Page keys that appear more than once (keys are the stable ids — hard error). */
|
|
738
|
+
duplicateKeys: string[];
|
|
739
|
+
/** Flow pages sharing a slug (URL ambiguity — warning, not folded into `ok`). */
|
|
740
|
+
slugCollisions: {
|
|
741
|
+
slug: string;
|
|
742
|
+
pages: string[];
|
|
743
|
+
}[];
|
|
744
|
+
/** Variant pages (`x@b`) whose slot (`x`) doesn't exist as a flow page (hard error). */
|
|
745
|
+
orphanVariants: string[];
|
|
746
|
+
/**
|
|
747
|
+
* True when no `finish` page is reachable from the start — e.g. the flow loops
|
|
748
|
+
* (a cycle with no exit) or every path stalls before a terminal page. Dead ends
|
|
749
|
+
* catch pages with no outgoing edge; this catches funnels that *never end* even
|
|
750
|
+
* though every page can proceed. Hard error.
|
|
751
|
+
*/
|
|
752
|
+
noReachableFinish: boolean;
|
|
753
|
+
}
|
|
754
|
+
interface FunnelManifest {
|
|
755
|
+
id: string;
|
|
756
|
+
pages: ManifestPage[];
|
|
757
|
+
flow: {
|
|
758
|
+
start: string | null;
|
|
759
|
+
nodes: string[];
|
|
760
|
+
edges: ManifestEdge[];
|
|
761
|
+
};
|
|
762
|
+
products: string[];
|
|
763
|
+
locales?: FunnelLocales;
|
|
764
|
+
validation: ManifestValidation;
|
|
765
|
+
}
|
|
766
|
+
interface CompileInput {
|
|
767
|
+
funnel: FunnelDefinition;
|
|
768
|
+
/** Pages in file order (the default flow); `meta` holds type/slug/next. */
|
|
769
|
+
pages: {
|
|
770
|
+
key: string;
|
|
771
|
+
meta?: PageMeta;
|
|
772
|
+
}[];
|
|
773
|
+
}
|
|
774
|
+
/** Compile a funnel's spine into its declarative {@link FunnelManifest} IR. */
|
|
775
|
+
declare function compileManifest(input: CompileInput): FunnelManifest;
|
|
776
|
+
|
|
777
|
+
export { evaluateCondition as $, type Acquisition as A, type BrowserContext as B, type ClickIdContext as C, type Direction as D, type EdgeCondition as E, type FunnelContext as F, type Gate as G, type Route as H, type IdentityContext as I, type SystemContext as J, assignVariant as K, type LocaleContext as L, type ManifestEdge as M, bucketingSeed as N, type OsContext as O, type Product as P, buildAcquisition as Q, type RuntimeExperiment as R, type SessionContext as S, buildCatalog as T, type UtmContext as U, type VariableValue as V, compileManifest as W, currencyExponent as X, defineFunnel as Y, definePage as Z, entryGuard as _, type VariableConfig as a, evaluateGate as a0, expectedPathLength as a1, fnv1a as a2, formatMoney as a3, formatProduct as a4, hashToUnit as a5, isRtl as a6, isVariantKey as a7, nextPage as a8, outgoingKeys as a9, pageMeta as aa, parseSlotKey as ab, pickByWeight as ac, resolveExperiments as ad, resolveLocale as ae, resolveProduct as af, resolveRoute as ag, validateExperiments as ah, type FunnelPage as ai, weightsOf as aj, type FunnelSnapshot as b, type FunnelDefinition as c, buildContext as d, type ProductInput as e, type PageMeta as f, type PageContext as g, type BuildContextOptions as h, type CompileInput as i, type Condition as j, type ConditionOp as k, type DeviceContext as l, type ExperimentIssue as m, type ExperimentResolution as n, type ExperimentValidation as o, type ExperimentVariant as p, type FlowPage as q, type FunnelLocales as r, type FunnelManifest as s, type Interval as t, type ManifestPage as u, type ManifestValidation as v, type Money as w, type PageType as x, type Predicate as y, type ResolvedProduct as z };
|