@appfunnel-dev/sdk 2.0.0-canary.1 → 2.0.0-canary.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/capabilities-7_hy5f5G.d.cts +114 -0
  2. package/dist/capabilities-7_hy5f5G.d.ts +114 -0
  3. package/dist/checkout-7Dy6IedP.d.ts +320 -0
  4. package/dist/checkout-Dz8cGkB_.d.cts +320 -0
  5. package/dist/chunk-AKO6XKXP.js +466 -0
  6. package/dist/chunk-AKO6XKXP.js.map +1 -0
  7. package/dist/chunk-CY4VBSMX.cjs +106 -0
  8. package/dist/chunk-CY4VBSMX.cjs.map +1 -0
  9. package/dist/chunk-JSRKA375.cjs +497 -0
  10. package/dist/chunk-JSRKA375.cjs.map +1 -0
  11. package/dist/chunk-LJYLGLFS.cjs +153 -0
  12. package/dist/chunk-LJYLGLFS.cjs.map +1 -0
  13. package/dist/chunk-M6U3FNRW.js +99 -0
  14. package/dist/chunk-M6U3FNRW.js.map +1 -0
  15. package/dist/chunk-YY375F2B.js +140 -0
  16. package/dist/chunk-YY375F2B.js.map +1 -0
  17. package/dist/driver-paddle.cjs +814 -0
  18. package/dist/driver-paddle.cjs.map +1 -0
  19. package/dist/driver-paddle.d.cts +10 -0
  20. package/dist/driver-paddle.d.ts +10 -0
  21. package/dist/driver-paddle.js +811 -0
  22. package/dist/driver-paddle.js.map +1 -0
  23. package/dist/driver-stripe.cjs +2253 -0
  24. package/dist/driver-stripe.cjs.map +1 -0
  25. package/dist/driver-stripe.d.cts +8 -0
  26. package/dist/driver-stripe.d.ts +8 -0
  27. package/dist/driver-stripe.js +2247 -0
  28. package/dist/driver-stripe.js.map +1 -0
  29. package/dist/index.cjs +1962 -811
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +183 -933
  32. package/dist/index.d.ts +183 -933
  33. package/dist/index.js +1683 -653
  34. package/dist/index.js.map +1 -1
  35. package/dist/manifest-Cr2y1op6.d.cts +814 -0
  36. package/dist/manifest-Cr2y1op6.d.ts +814 -0
  37. package/dist/manifest-entry.cjs +312 -0
  38. package/dist/manifest-entry.cjs.map +1 -0
  39. package/dist/manifest-entry.d.cts +209 -0
  40. package/dist/manifest-entry.d.ts +209 -0
  41. package/dist/manifest-entry.js +198 -0
  42. package/dist/manifest-entry.js.map +1 -0
  43. package/package.json +37 -4
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ var chunkLJYLGLFS_cjs = require('./chunk-LJYLGLFS.cjs');
4
+
5
+ // src/drivers/wire.ts
6
+ function endpoint(ctx, path = "") {
7
+ const base = ctx.apiBase.replace(/\/+$/, "");
8
+ return `${base}/campaign/${encodeURIComponent(ctx.campaignId)}/v2/checkout${path}`;
9
+ }
10
+ async function postJson(url, body) {
11
+ const res = await fetch(url, {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json" },
14
+ body: JSON.stringify(body)
15
+ });
16
+ if (!res.ok) throw chunkLJYLGLFS_cjs.checkoutError("processing_error", `checkout request failed (HTTP ${res.status})`);
17
+ return await res.json();
18
+ }
19
+ function toCheckoutError(e) {
20
+ if (e && typeof e === "object" && "category" in e && "message" in e && "retryable" in e) {
21
+ return e;
22
+ }
23
+ return chunkLJYLGLFS_cjs.checkoutError("processing_error", e instanceof Error ? e.message : "Checkout request failed");
24
+ }
25
+ function postSession(ctx, req, correlationId) {
26
+ const body = {
27
+ funnelId: ctx.funnelId,
28
+ mode: ctx.mode,
29
+ // The CHARGE identity is the catalog key (resolved from the slot by driverWithCatalog); fall back
30
+ // to `product` for legacy builds where the page named the catalog key directly.
31
+ productKey: req.catalogKey ?? req.product,
32
+ intent: req.intent,
33
+ kind: req.kind,
34
+ // null (not absent) = explicit off-session — the server validates surfaces
35
+ // against the capability matrix, so never let one default in transit.
36
+ surface: req.surface ?? null,
37
+ sessionId: ctx.sessionId ?? void 0,
38
+ visitorId: ctx.visitorId ?? void 0,
39
+ // Buyer email collected by the funnel (user.email) — Stripe Customer creation,
40
+ // receipts, Paddle prefill. Injected at attempt time by the FunnelProvider seam.
41
+ email: req.email,
42
+ correlationId
43
+ };
44
+ return postJson(endpoint(ctx), body);
45
+ }
46
+ function postComplete(ctx, correlationId, providerRef) {
47
+ const body = { providerRef };
48
+ return postJson(
49
+ endpoint(ctx, `/${encodeURIComponent(correlationId)}/complete`),
50
+ body
51
+ );
52
+ }
53
+ function settlementToResult(s) {
54
+ return { ok: true, amountMinor: s.amountMinor, currency: s.currency, eventId: s.eventId };
55
+ }
56
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
57
+ async function completeWithPoll(ctx, correlationId, opts = {}) {
58
+ const attempts = opts.attempts ?? 5;
59
+ const intervalMs = opts.intervalMs ?? 2e3;
60
+ try {
61
+ for (let i = 0; i < attempts; i++) {
62
+ const res = await postComplete(ctx, correlationId, opts.providerRef);
63
+ if (res.step === "settled") return settlementToResult(res.result);
64
+ if (res.step === "failed") return { ok: false, error: res.error };
65
+ if (i < attempts - 1) await sleep(intervalMs);
66
+ }
67
+ } catch (e) {
68
+ return { ok: false, error: toCheckoutError(e) };
69
+ }
70
+ return {
71
+ ok: false,
72
+ error: chunkLJYLGLFS_cjs.checkoutError(
73
+ "processing_error",
74
+ opts.timeoutMessage ?? "The payment has not been confirmed yet \u2014 please check back shortly.",
75
+ // `completion_timeout` marks "the poll ran out while the attempt was still
76
+ // pending" — drivers key double-charge guards on it (the attempt may still
77
+ // settle server-side; it was never refused).
78
+ { retryable: true, declineCode: "completion_timeout" }
79
+ )
80
+ };
81
+ }
82
+ function leaveForRedirect(url) {
83
+ return new Promise((resolve) => {
84
+ if (typeof window !== "undefined") {
85
+ const onPageshow = (event) => {
86
+ if (!event.persisted) return;
87
+ window.removeEventListener("pageshow", onPageshow);
88
+ resolve({
89
+ ok: false,
90
+ error: chunkLJYLGLFS_cjs.checkoutError("canceled", "Returned from the hosted checkout without completing")
91
+ });
92
+ };
93
+ window.addEventListener("pageshow", onPageshow);
94
+ }
95
+ window.location.assign(url);
96
+ });
97
+ }
98
+
99
+ exports.completeWithPoll = completeWithPoll;
100
+ exports.leaveForRedirect = leaveForRedirect;
101
+ exports.postComplete = postComplete;
102
+ exports.postSession = postSession;
103
+ exports.settlementToResult = settlementToResult;
104
+ exports.toCheckoutError = toCheckoutError;
105
+ //# sourceMappingURL=chunk-CY4VBSMX.cjs.map
106
+ //# sourceMappingURL=chunk-CY4VBSMX.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/drivers/wire.ts"],"names":["checkoutError"],"mappings":"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","file":"chunk-CY4VBSMX.cjs","sourcesContent":["/**\n * Shared wire plumbing for the checkout drivers — the CLIENT half of the v2\n * checkout contract (commerce/checkout-api.ts). Lives under drivers/ (not\n * commerce/) on purpose: it ships only inside the `driver-*` subpath entries,\n * so the core funnel entry never bundles a checkout fetch — the core SDK talks\n * to a {@link CheckoutDriver}, never to the wire.\n *\n * Only `import type` from commerce/checkout(.tsx) here — a runtime import would\n * drag the React checkout primitives into every driver chunk. The one runtime\n * dependency is the pure error taxonomy (capabilities.ts).\n */\nimport type {\n CheckoutCompleteRequest,\n CheckoutCompleteResponse,\n CheckoutSessionRequest,\n CheckoutSessionResponse,\n CheckoutSettlement,\n} from '../commerce/checkout-api'\nimport type { CheckoutDriverContext, CheckoutRequest, CheckoutResult } from '../commerce/checkout'\nimport { checkoutError, type CheckoutError } from '../commerce/capabilities'\n\nfunction endpoint(ctx: CheckoutDriverContext, path = ''): string {\n const base = ctx.apiBase.replace(/\\/+$/, '')\n return `${base}/campaign/${encodeURIComponent(ctx.campaignId)}/v2/checkout${path}`\n}\n\nasync function postJson<T>(url: string, body: unknown): Promise<T> {\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n // The contract speaks in `step` responses, not HTTP errors — a non-2xx means the\n // request never reached the checkout service properly (proxy, deploy, outage).\n if (!res.ok) throw checkoutError('processing_error', `checkout request failed (HTTP ${res.status})`)\n return (await res.json()) as T\n}\n\n/** Normalize any thrown value into the taxonomy (network drops → `processing_error`). */\nexport function toCheckoutError(e: unknown): CheckoutError {\n if (e && typeof e === 'object' && 'category' in e && 'message' in e && 'retryable' in e) {\n return e as CheckoutError\n }\n return checkoutError('processing_error', e instanceof Error ? e.message : 'Checkout request failed')\n}\n\n/**\n * Open a checkout attempt: `POST {apiBase}/campaign/{campaignId}/v2/checkout`.\n * The correlationId is client-minted per ATTEMPT (idempotency key + the id the\n * server stamps into provider metadata) — mint it once, reuse it on retries.\n */\nexport function postSession(\n ctx: CheckoutDriverContext,\n req: CheckoutRequest,\n correlationId: string,\n): Promise<CheckoutSessionResponse> {\n const body: CheckoutSessionRequest = {\n funnelId: ctx.funnelId,\n mode: ctx.mode,\n // The CHARGE identity is the catalog key (resolved from the slot by driverWithCatalog); fall back\n // to `product` for legacy builds where the page named the catalog key directly.\n productKey: req.catalogKey ?? req.product,\n intent: req.intent,\n kind: req.kind,\n // null (not absent) = explicit off-session — the server validates surfaces\n // against the capability matrix, so never let one default in transit.\n surface: req.surface ?? null,\n sessionId: ctx.sessionId ?? undefined,\n visitorId: ctx.visitorId ?? undefined,\n // Buyer email collected by the funnel (user.email) — Stripe Customer creation,\n // receipts, Paddle prefill. Injected at attempt time by the FunnelProvider seam.\n email: req.email,\n correlationId,\n }\n return postJson<CheckoutSessionResponse>(endpoint(ctx), body)\n}\n\n/** Ask the server to VERIFY with the provider and write the money facts (idempotent). */\nexport function postComplete(\n ctx: CheckoutDriverContext,\n correlationId: string,\n providerRef?: string,\n): Promise<CheckoutCompleteResponse> {\n const body: CheckoutCompleteRequest = { providerRef }\n return postJson<CheckoutCompleteResponse>(\n endpoint(ctx, `/${encodeURIComponent(correlationId)}/complete`),\n body,\n )\n}\n\n/** Server settlement → the SDK's CheckoutResult (eventId passes through for pixel dedup). */\nexport function settlementToResult(s: CheckoutSettlement): CheckoutResult {\n return { ok: true, amountMinor: s.amountMinor, currency: s.currency, eventId: s.eventId }\n}\n\nexport interface CompletePollOptions {\n providerRef?: string\n /** Total complete attempts before giving up (default 5). */\n attempts?: number\n /** Delay between attempts (default 2000ms). */\n intervalMs?: number\n /** Error message when the provider hasn't confirmed inside the window (MoR/async). */\n timeoutMessage?: string\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\n\n/**\n * Complete with a bounded poll. Sync providers (Stripe) settle on the first\n * attempt — the server verifies against the provider inline. MoR/async providers\n * (Paddle) settle via webhook, so `pending` is normal: re-poll a few times, then\n * stop holding the UI hostage — the charge may still land server-side, which is\n * why the timeout error is `retryable` and the message should say the receipt\n * can still arrive (the server's money facts don't depend on this browser).\n */\nexport async function completeWithPoll(\n ctx: CheckoutDriverContext,\n correlationId: string,\n opts: CompletePollOptions = {},\n): Promise<CheckoutResult> {\n const attempts = opts.attempts ?? 5\n const intervalMs = opts.intervalMs ?? 2000\n try {\n for (let i = 0; i < attempts; i++) {\n const res = await postComplete(ctx, correlationId, opts.providerRef)\n if (res.step === 'settled') return settlementToResult(res.result)\n if (res.step === 'failed') return { ok: false, error: res.error }\n if (i < attempts - 1) await sleep(intervalMs)\n }\n } catch (e) {\n return { ok: false, error: toCheckoutError(e) }\n }\n return {\n ok: false,\n error: checkoutError(\n 'processing_error',\n opts.timeoutMessage ?? 'The payment has not been confirmed yet — please check back shortly.',\n // `completion_timeout` marks \"the poll ran out while the attempt was still\n // pending\" — drivers key double-charge guards on it (the attempt may still\n // settle server-side; it was never refused).\n { retryable: true, declineCode: 'completion_timeout' },\n ),\n }\n}\n\n/**\n * The `redirect` step hand-off: navigate to the provider-hosted page and return a\n * promise that settles ONLY if the buyer comes back to this very page. The page\n * is leaving — resolving early would fire `purchase.complete`/navigation for a\n * charge that hasn't happened, and rejecting would flash an error during the\n * (successful) hand-off — so while the navigation proceeds the promise stays\n * pending and `useCheckout` stays in `loading`, the truthful UI for a page\n * mid-navigation. The hosted page's SUCCESS return URL re-enters the funnel with\n * `af_checkout={correlationId}` (a fresh load — this promise is gone; the SDK's\n * resume leg settles the attempt).\n *\n * The one way THIS page comes back to life is a bfcache restore (browser Back\n * from the hosted page): `pageshow` with `persisted: true`. That is a buyer who\n * abandoned the hosted checkout — resolve `canceled` so the trigger re-enables\n * and inline redirect placeholders recover instead of spinning forever.\n */\nexport function leaveForRedirect(url: string): Promise<CheckoutResult> {\n return new Promise<CheckoutResult>((resolve) => {\n if (typeof window !== 'undefined') {\n const onPageshow = (event: PageTransitionEvent) => {\n if (!event.persisted) return\n window.removeEventListener('pageshow', onPageshow)\n resolve({\n ok: false,\n error: checkoutError('canceled', 'Returned from the hosted checkout without completing'),\n })\n }\n window.addEventListener('pageshow', onPageshow)\n }\n window.location.assign(url)\n })\n}\n"]}
@@ -0,0 +1,497 @@
1
+ 'use strict';
2
+
3
+ // src/i18n/locale.ts
4
+ var RTL_LANGS = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "ps", "sd", "dv", "yi"]);
5
+ function isRtl(locale) {
6
+ return RTL_LANGS.has(locale.split("-")[0]);
7
+ }
8
+ var base = (l) => l?.split("-")[0];
9
+ function resolveLocale(config, detected, override) {
10
+ const supported = config?.supported ?? (config?.default ? [config.default] : ["en"]);
11
+ const def = config?.default ?? supported[0] ?? "en";
12
+ const pick = (l) => {
13
+ if (!l) return void 0;
14
+ if (supported.includes(l)) return l;
15
+ return supported.find((s) => base(s) === base(l));
16
+ };
17
+ return pick(override) ?? pick(detected) ?? def;
18
+ }
19
+
20
+ // src/commerce/money.ts
21
+ var PERIOD_DAYS = {
22
+ day: 1,
23
+ week: 7,
24
+ month: 365 / 12,
25
+ year: 365,
26
+ one_time: 0
27
+ };
28
+ function periodDays(interval, count) {
29
+ return PERIOD_DAYS[interval] * count;
30
+ }
31
+ function intervalLabel(interval, count) {
32
+ if (interval === "one_time") return { period: "one-time", periodly: "one-time" };
33
+ if (interval === "month" && count === 3) return { period: "quarter", periodly: "quarterly" };
34
+ if (interval === "month" && count === 6) return { period: "6 months", periodly: "semiannually" };
35
+ if (interval === "week" && count === 2) return { period: "2 weeks", periodly: "biweekly" };
36
+ if (count === 1) {
37
+ const periodly = { day: "daily", week: "weekly", month: "monthly", year: "yearly" }[interval];
38
+ return { period: interval, periodly };
39
+ }
40
+ return { period: `${count} ${interval}s`, periodly: `every ${count} ${interval}s` };
41
+ }
42
+ function resolveProduct(input) {
43
+ const interval = input.interval ?? "one_time";
44
+ const count = input.intervalCount ?? 1;
45
+ const days = periodDays(interval, count);
46
+ const per = (targetDays) => days > 0 ? Math.round(input.amount * targetDays / days) : input.amount;
47
+ const label = intervalLabel(interval, count);
48
+ const trialDays = input.trialDays ?? 0;
49
+ const trialMinor = input.trialAmount ?? 0;
50
+ return {
51
+ id: input.id,
52
+ catalogKey: input.catalogKey ?? input.id,
53
+ name: input.name ?? input.id,
54
+ displayName: input.displayName ?? input.name ?? input.id,
55
+ provider: input.provider ?? "stripe",
56
+ currency: input.currency,
57
+ priceMinor: input.amount,
58
+ perDayMinor: per(1),
59
+ perWeekMinor: per(7),
60
+ perMonthMinor: per(365 / 12),
61
+ perYearMinor: per(365),
62
+ period: label.period,
63
+ periodly: label.periodly,
64
+ hasTrial: trialDays > 0 || trialMinor > 0,
65
+ trialDays,
66
+ trialMinor
67
+ };
68
+ }
69
+ function buildCatalog(inputs) {
70
+ return new Map(inputs.map((i) => [i.id, resolveProduct(i)]));
71
+ }
72
+ var EXPONENT_CACHE = /* @__PURE__ */ new Map();
73
+ function currencyExponent(currency) {
74
+ const code = currency.toUpperCase();
75
+ const hit = EXPONENT_CACHE.get(code);
76
+ if (hit !== void 0) return hit;
77
+ let exp = 2;
78
+ try {
79
+ exp = new Intl.NumberFormat("en", { style: "currency", currency: code }).resolvedOptions().maximumFractionDigits ?? 2;
80
+ } catch {
81
+ exp = 2;
82
+ }
83
+ EXPONENT_CACHE.set(code, exp);
84
+ return exp;
85
+ }
86
+ function formatMoney(minor, currency, locale) {
87
+ const code = currency.toUpperCase();
88
+ const exp = currencyExponent(code);
89
+ const amount = minor / 10 ** exp;
90
+ let formatted;
91
+ try {
92
+ formatted = new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(amount);
93
+ } catch {
94
+ formatted = `${code} ${amount.toFixed(exp)}`;
95
+ }
96
+ return { amount, minor, currency: code, formatted };
97
+ }
98
+ function formatProduct(r, locale) {
99
+ const m = (minor) => formatMoney(minor, r.currency, locale);
100
+ return {
101
+ id: r.id,
102
+ catalogKey: r.catalogKey,
103
+ name: r.name,
104
+ displayName: r.displayName,
105
+ provider: r.provider,
106
+ price: m(r.priceMinor),
107
+ period: r.period,
108
+ periodly: r.periodly,
109
+ perDay: m(r.perDayMinor),
110
+ perWeek: m(r.perWeekMinor),
111
+ perMonth: m(r.perMonthMinor),
112
+ perYear: m(r.perYearMinor),
113
+ hasTrial: r.hasTrial,
114
+ trialDays: r.trialDays,
115
+ trialPrice: r.hasTrial ? m(r.trialMinor) : null
116
+ };
117
+ }
118
+
119
+ // src/flow/spine.ts
120
+ function readField(s, field) {
121
+ let cur = s;
122
+ for (const part of field.split(".")) {
123
+ if (cur == null) return void 0;
124
+ cur = cur[part];
125
+ }
126
+ return cur;
127
+ }
128
+ function evaluateCondition(cond, s) {
129
+ const val = readField(s, cond.field);
130
+ const { op, value } = cond;
131
+ switch (op) {
132
+ case "eq":
133
+ return val === value;
134
+ case "neq":
135
+ return val !== value;
136
+ case "in":
137
+ return Array.isArray(value) && value.includes(val);
138
+ case "gt":
139
+ return typeof val === "number" && typeof value === "number" && val > value;
140
+ case "gte":
141
+ return typeof val === "number" && typeof value === "number" && val >= value;
142
+ case "lt":
143
+ return typeof val === "number" && typeof value === "number" && val < value;
144
+ case "lte":
145
+ return typeof val === "number" && typeof value === "number" && val <= value;
146
+ case "contains":
147
+ if (typeof val === "string") return val.includes(String(value));
148
+ if (Array.isArray(val)) return val.includes(value);
149
+ return false;
150
+ case "exists":
151
+ return val !== void 0 && val !== null && val !== "";
152
+ case "empty":
153
+ return val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
154
+ default:
155
+ return false;
156
+ }
157
+ }
158
+ function evaluateGate(gate, s) {
159
+ return typeof gate === "function" ? gate(s) : evaluateCondition(gate, s);
160
+ }
161
+ function resolveRoute(routes, s) {
162
+ if (!routes) return void 0;
163
+ for (const route of routes) {
164
+ if (!route.when || evaluateGate(route.when, s)) return route.to;
165
+ }
166
+ return void 0;
167
+ }
168
+ function pageMeta(meta) {
169
+ return meta;
170
+ }
171
+ var TYPE_GUARDS = {
172
+ paywall: { field: "user.email", op: "exists" },
173
+ upsell: { field: "user.email", op: "exists" }
174
+ };
175
+ function entryGuard(meta) {
176
+ return meta?.guard ?? (meta?.type ? TYPE_GUARDS[meta.type] : void 0);
177
+ }
178
+ function definePage(component) {
179
+ return component;
180
+ }
181
+ function nextPage(pages, currentKey, s) {
182
+ const idx = pages.findIndex((p) => p.key === currentKey);
183
+ if (idx === -1) return void 0;
184
+ const routed = resolveRoute(pages[idx].meta?.next, s);
185
+ if (routed) return routed;
186
+ return pages[idx + 1]?.key;
187
+ }
188
+ function outgoingKeys(pages, currentKey) {
189
+ const idx = pages.findIndex((p) => p.key === currentKey);
190
+ if (idx === -1) return [];
191
+ const out = /* @__PURE__ */ new Set();
192
+ for (const route of pages[idx].meta?.next ?? []) out.add(route.to);
193
+ const linear = pages[idx + 1]?.key;
194
+ if (linear) out.add(linear);
195
+ return [...out];
196
+ }
197
+ function expectedPathLength(pages, startKey, s) {
198
+ const seen = /* @__PURE__ */ new Set();
199
+ let key = startKey;
200
+ let count = 0;
201
+ while (key && !seen.has(key)) {
202
+ seen.add(key);
203
+ count++;
204
+ if (pages.find((p) => p.key === key)?.meta?.type === "finish") break;
205
+ key = nextPage(pages, key, s);
206
+ }
207
+ return count;
208
+ }
209
+ function defineFunnel(def) {
210
+ return def;
211
+ }
212
+ function normalizeProducts(products) {
213
+ if (!products) return [];
214
+ if (Array.isArray(products)) return products.map((k) => ({ slot: k, catalogKey: k }));
215
+ return Object.entries(products).map(([slot, catalogKey]) => ({ slot, catalogKey: catalogKey ?? null }));
216
+ }
217
+ function funnelCatalogKeys(products) {
218
+ return [...new Set(normalizeProducts(products).map((s) => s.catalogKey).filter((k) => !!k))];
219
+ }
220
+
221
+ // src/flow/experiments.ts
222
+ function fnv1a(input) {
223
+ let h = 2166136261;
224
+ for (let i = 0; i < input.length; i++) {
225
+ h ^= input.charCodeAt(i);
226
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
227
+ }
228
+ return h >>> 0;
229
+ }
230
+ function hashToUnit(seed) {
231
+ return fnv1a(seed) / 4294967296;
232
+ }
233
+ function pickByWeight(weights, u) {
234
+ const entries = Object.entries(weights).filter(([, w]) => w > 0);
235
+ if (entries.length === 0) return Object.keys(weights)[0] ?? "";
236
+ const total = entries.reduce((sum, [, w]) => sum + w, 0);
237
+ const target = u * total;
238
+ let acc = 0;
239
+ for (const [key, w] of entries) {
240
+ acc += w;
241
+ if (target < acc) return key;
242
+ }
243
+ return entries[entries.length - 1][0];
244
+ }
245
+ function assignVariant(experimentId, seed, weights) {
246
+ return pickByWeight(weights, hashToUnit(`${experimentId}:${seed}`));
247
+ }
248
+ function bucketingSeed(context) {
249
+ return context.identity.visitorId ?? context.identity.customerId ?? null;
250
+ }
251
+ function parseSlotKey(key) {
252
+ const at = key.indexOf("@");
253
+ return at === -1 ? { slot: key } : { slot: key.slice(0, at), variant: key.slice(at + 1) };
254
+ }
255
+ function isVariantKey(key) {
256
+ return parseSlotKey(key).variant !== void 0;
257
+ }
258
+ function weightsOf(variants) {
259
+ const out = {};
260
+ for (const [label, v] of Object.entries(variants)) out[label] = v.weight;
261
+ return out;
262
+ }
263
+ function statusOf(exp) {
264
+ return exp.status ?? "running";
265
+ }
266
+ function resolveExperiments(pages, experiments, seed) {
267
+ const assignments = {};
268
+ const render = {};
269
+ const experimentOf = {};
270
+ const versionOf = {};
271
+ const pageKeys = new Set(pages.map((p) => p.key));
272
+ for (const exp of experiments) {
273
+ const status = statusOf(exp);
274
+ let label;
275
+ if (status === "stopped" && exp.winner) label = exp.winner;
276
+ else if (status === "running" && seed) label = assignVariant(exp.id, seed, weightsOf(exp.variants));
277
+ else continue;
278
+ const arm = exp.variants[label];
279
+ if (!arm) continue;
280
+ if (!pageKeys.has(arm.page) || !pageKeys.has(exp.slot)) continue;
281
+ assignments[exp.id] = label;
282
+ experimentOf[exp.slot] = exp.id;
283
+ if (exp.version !== void 0) versionOf[exp.id] = exp.version;
284
+ if (arm.page !== exp.slot) render[exp.slot] = arm.page;
285
+ }
286
+ const slotOf = {};
287
+ const activeKeys = [];
288
+ for (const p of pages) {
289
+ const { slot, variant } = parseSlotKey(p.key);
290
+ if (variant !== void 0) slotOf[p.key] = slot;
291
+ else activeKeys.push(p.key);
292
+ }
293
+ return { assignments, activeKeys, render, slotOf, experimentOf, versionOf };
294
+ }
295
+ function validateExperiments(experiments, pages) {
296
+ const errors = [];
297
+ const warnings = [];
298
+ const pageKeys = new Set(pages.map((p) => p.key));
299
+ const seenIds = /* @__PURE__ */ new Set();
300
+ const slotOwner = /* @__PURE__ */ new Map();
301
+ for (const exp of experiments) {
302
+ const err = (code, message) => errors.push({ experimentId: exp.id, code, message });
303
+ const warn = (code, message) => warnings.push({ experimentId: exp.id, code, message });
304
+ if (seenIds.has(exp.id)) err("duplicate_id", `Experiment id "${exp.id}" is used more than once.`);
305
+ seenIds.add(exp.id);
306
+ if (!pageKeys.has(exp.slot)) err("slot_missing", `Slot "${exp.slot}" is not a page in the funnel.`);
307
+ if (isVariantKey(exp.slot)) err("slot_is_variant", `Slot "${exp.slot}" is a variant page; a slot must be a real flow page.`);
308
+ if (slotOwner.has(exp.slot)) {
309
+ err("slot_conflict", `Slot "${exp.slot}" is already targeted by experiment "${slotOwner.get(exp.slot)}".`);
310
+ } else {
311
+ slotOwner.set(exp.slot, exp.id);
312
+ }
313
+ const arms = Object.entries(exp.variants);
314
+ if (arms.length < 2) warn("single_arm", `Experiment "${exp.id}" has fewer than two variants \u2014 nothing to test.`);
315
+ let hasControl = false;
316
+ let totalWeight = 0;
317
+ for (const [label, arm] of arms) {
318
+ if (!pageKeys.has(arm.page)) {
319
+ err("variant_page_missing", `Variant "${label}" references page "${arm.page}", which doesn't exist.`);
320
+ }
321
+ if (arm.page === exp.slot) hasControl = true;
322
+ else if (isVariantKey(arm.page) && parseSlotKey(arm.page).slot !== exp.slot) {
323
+ err("variant_slot_mismatch", `Variant "${label}" page "${arm.page}" belongs to a different slot than "${exp.slot}".`);
324
+ }
325
+ if (arm.weight < 0) err("negative_weight", `Variant "${label}" has a negative weight (${arm.weight}).`);
326
+ totalWeight += arm.weight;
327
+ }
328
+ if (!hasControl) err("no_control", `No variant of "${exp.id}" renders the slot "${exp.slot}" itself (the control/baseline).`);
329
+ if (totalWeight <= 0) err("no_traffic", `Experiment "${exp.id}" has no positive weight \u2014 no traffic would enter it.`);
330
+ if (statusOf(exp) === "stopped" && exp.winner && !exp.variants[exp.winner]) {
331
+ err("bad_winner", `Stopped experiment "${exp.id}" names winner "${exp.winner}", which isn't one of its variants.`);
332
+ }
333
+ }
334
+ const running = new Set(experiments.filter((e) => statusOf(e) === "running").flatMap(
335
+ (e) => Object.values(e.variants).map((v) => v.page)
336
+ ));
337
+ for (const p of pages) {
338
+ if (isVariantKey(p.key) && !running.has(p.key)) {
339
+ warnings.push({ experimentId: "*", code: "orphan_variant", message: `Variant page "${p.key}" is rendered by no running experiment.` });
340
+ }
341
+ }
342
+ return { ok: errors.length === 0, errors, warnings };
343
+ }
344
+
345
+ // src/manifest/manifest.ts
346
+ function isVariantNode(page) {
347
+ return isVariantKey(page.key);
348
+ }
349
+ function serializeGate(when) {
350
+ if (!when) return void 0;
351
+ if (typeof when === "function") return { kind: "predicate" };
352
+ return { kind: "declarative", condition: when };
353
+ }
354
+ function compileManifest(input) {
355
+ const { funnel, pages } = input;
356
+ const keys = new Set(pages.map((p) => p.key));
357
+ const manifestPages = pages.map((p, index) => ({
358
+ id: p.key,
359
+ key: p.key,
360
+ type: p.meta?.type ?? "default",
361
+ slug: p.meta?.slug ?? p.key,
362
+ index,
363
+ variantOf: isVariantKey(p.key) ? parseSlotKey(p.key).slot : void 0
364
+ }));
365
+ const edges = [];
366
+ const badTargets = [];
367
+ const nextAnchor = (from) => {
368
+ for (let j = from + 1; j < pages.length; j++) {
369
+ if (!isVariantNode(pages[j])) return pages[j];
370
+ }
371
+ return void 0;
372
+ };
373
+ pages.forEach((p, i) => {
374
+ if (isVariantNode(p)) return;
375
+ const routes = p.meta?.next ?? [];
376
+ let hasUnconditional = false;
377
+ for (const route of routes) {
378
+ if (!keys.has(route.to)) {
379
+ badTargets.push({ from: p.key, to: route.to });
380
+ continue;
381
+ }
382
+ edges.push({ from: p.key, to: route.to, kind: "branch", condition: serializeGate(route.when) });
383
+ if (!route.when) hasUnconditional = true;
384
+ }
385
+ const next = nextAnchor(i);
386
+ if (!hasUnconditional && next && p.meta?.type !== "finish") {
387
+ edges.push({ from: p.key, to: next.key, kind: "linear" });
388
+ }
389
+ });
390
+ const startPage = pages.find((p) => !isVariantNode(p));
391
+ const start = startPage?.key ?? null;
392
+ const variantKeys = new Set(pages.filter(isVariantNode).map((p) => p.key));
393
+ const productSlots = normalizeProducts(funnel.products);
394
+ const validation = validate(manifestPages, edges, badTargets, start, variantKeys, productSlots, input.productRefs);
395
+ return {
396
+ id: funnel.id,
397
+ pages: manifestPages,
398
+ flow: { start, nodes: manifestPages.map((p) => p.id), edges },
399
+ products: funnelCatalogKeys(funnel.products),
400
+ productSlots,
401
+ productRefs: input.productRefs,
402
+ locales: funnel.locales,
403
+ validation
404
+ };
405
+ }
406
+ function validate(pages, edges, badTargets, start, variantKeys, slots, productRefs) {
407
+ const outgoing = /* @__PURE__ */ new Map();
408
+ for (const e of edges) outgoing.set(e.from, (outgoing.get(e.from) ?? 0) + 1);
409
+ const deadEnds = pages.filter((p) => p.type !== "finish" && !variantKeys.has(p.id) && !(outgoing.get(p.id) > 0)).map((p) => p.id);
410
+ const reachable = /* @__PURE__ */ new Set();
411
+ if (start) {
412
+ const adj = /* @__PURE__ */ new Map();
413
+ for (const e of edges) {
414
+ const list = adj.get(e.from);
415
+ if (list) list.push(e.to);
416
+ else adj.set(e.from, [e.to]);
417
+ }
418
+ const stack = [start];
419
+ while (stack.length) {
420
+ const id = stack.pop();
421
+ if (reachable.has(id)) continue;
422
+ reachable.add(id);
423
+ for (const to of adj.get(id) ?? []) stack.push(to);
424
+ }
425
+ }
426
+ const unreachable = pages.filter((p) => !variantKeys.has(p.id) && !reachable.has(p.id)).map((p) => p.id);
427
+ const missingEmailCapture = !pages.some((p) => p.type === "email-capture");
428
+ const seen = /* @__PURE__ */ new Set();
429
+ const duplicateKeys = [];
430
+ for (const p of pages) {
431
+ if (seen.has(p.key) && !duplicateKeys.includes(p.key)) duplicateKeys.push(p.key);
432
+ seen.add(p.key);
433
+ }
434
+ const bySlug = /* @__PURE__ */ new Map();
435
+ for (const p of pages) {
436
+ if (variantKeys.has(p.id)) continue;
437
+ const list = bySlug.get(p.slug);
438
+ if (list) list.push(p.id);
439
+ else bySlug.set(p.slug, [p.id]);
440
+ }
441
+ const slugCollisions = [...bySlug.entries()].filter(([, ids]) => ids.length > 1).map(([slug, ids]) => ({ slug, pages: ids }));
442
+ const flowKeys = new Set(pages.filter((p) => !variantKeys.has(p.id)).map((p) => p.key));
443
+ const orphanVariants = pages.filter((p) => p.variantOf !== void 0 && !flowKeys.has(p.variantOf)).map((p) => p.id);
444
+ const noReachableFinish = start !== null && !pages.some((p) => p.type === "finish" && reachable.has(p.id));
445
+ const declaredSlots = new Set(slots.map((s) => s.slot));
446
+ const assignedSlots = new Set(slots.filter((s) => s.catalogKey).map((s) => s.slot));
447
+ const refs = productRefs ?? [];
448
+ const unknownProductRefs = [...new Set(refs.filter((r) => !declaredSlots.has(r)))];
449
+ const unassignedSlots = [...new Set(refs.filter((r) => declaredSlots.has(r) && !assignedSlots.has(r)))];
450
+ return {
451
+ // Original hard errors AND the structural ones; slug collisions stay a warning.
452
+ ok: deadEnds.length === 0 && unreachable.length === 0 && badTargets.length === 0 && !missingEmailCapture && duplicateKeys.length === 0 && orphanVariants.length === 0 && !noReachableFinish && unknownProductRefs.length === 0 && unassignedSlots.length === 0,
453
+ deadEnds,
454
+ unreachable,
455
+ badTargets,
456
+ missingEmailCapture,
457
+ duplicateKeys,
458
+ slugCollisions,
459
+ orphanVariants,
460
+ noReachableFinish,
461
+ unknownProductRefs,
462
+ unassignedSlots
463
+ };
464
+ }
465
+
466
+ exports.assignVariant = assignVariant;
467
+ exports.bucketingSeed = bucketingSeed;
468
+ exports.buildCatalog = buildCatalog;
469
+ exports.compileManifest = compileManifest;
470
+ exports.currencyExponent = currencyExponent;
471
+ exports.defineFunnel = defineFunnel;
472
+ exports.definePage = definePage;
473
+ exports.entryGuard = entryGuard;
474
+ exports.evaluateCondition = evaluateCondition;
475
+ exports.evaluateGate = evaluateGate;
476
+ exports.expectedPathLength = expectedPathLength;
477
+ exports.fnv1a = fnv1a;
478
+ exports.formatMoney = formatMoney;
479
+ exports.formatProduct = formatProduct;
480
+ exports.funnelCatalogKeys = funnelCatalogKeys;
481
+ exports.hashToUnit = hashToUnit;
482
+ exports.isRtl = isRtl;
483
+ exports.isVariantKey = isVariantKey;
484
+ exports.nextPage = nextPage;
485
+ exports.normalizeProducts = normalizeProducts;
486
+ exports.outgoingKeys = outgoingKeys;
487
+ exports.pageMeta = pageMeta;
488
+ exports.parseSlotKey = parseSlotKey;
489
+ exports.pickByWeight = pickByWeight;
490
+ exports.resolveExperiments = resolveExperiments;
491
+ exports.resolveLocale = resolveLocale;
492
+ exports.resolveProduct = resolveProduct;
493
+ exports.resolveRoute = resolveRoute;
494
+ exports.validateExperiments = validateExperiments;
495
+ exports.weightsOf = weightsOf;
496
+ //# sourceMappingURL=chunk-JSRKA375.cjs.map
497
+ //# sourceMappingURL=chunk-JSRKA375.cjs.map