@appfunnel-dev/sdk 0.17.0 → 2.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,1709 +1,2112 @@
1
1
  'use strict';
2
2
 
3
- var chunk7D7VJBOV_cjs = require('./chunk-7D7VJBOV.cjs');
4
- var chunk7BSBNWJ6_cjs = require('./chunk-7BSBNWJ6.cjs');
5
3
  var react = require('react');
6
- var stripeJs = require('@stripe/stripe-js');
4
+ var reactDom = require('react-dom');
7
5
  var sonner = require('sonner');
8
- var reactStripeJs = require('@stripe/react-stripe-js');
9
- var jsxRuntime = require('react/jsx-runtime');
10
6
 
11
- // src/config.ts
12
- function defineConfig(config) {
13
- return config;
14
- }
15
- function definePage(definition) {
16
- return definition;
17
- }
18
- function useVariable(id) {
19
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
20
- const subscribe = react.useCallback(
21
- (callback) => variableStore.subscribe(callback, { keys: [id] }),
22
- [variableStore, id]
23
- );
24
- const getSnapshot = react.useCallback(
25
- () => variableStore.get(id),
26
- [variableStore, id]
27
- );
28
- const value = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
29
- const setValue = react.useCallback(
30
- (v) => variableStore.set(id, v),
31
- [variableStore, id]
32
- );
33
- return [value, setValue];
34
- }
35
- function useVariables() {
36
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
37
- const subscribe = react.useCallback(
38
- (callback) => variableStore.subscribe(callback),
39
- [variableStore]
40
- );
41
- const getSnapshot = react.useCallback(
42
- () => variableStore.getState(),
43
- [variableStore]
44
- );
45
- return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
46
- }
7
+ // src/state/namespaces.ts
47
8
 
48
- // src/utils/date.ts
49
- function toISODate(input) {
50
- if (!input || !input.trim()) return "";
51
- const s = input.trim();
52
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
53
- if (/^\d{4}-\d{2}-\d{2}T/.test(s)) return s.slice(0, 10);
54
- const sepMatch = s.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
55
- if (sepMatch) {
56
- const a = parseInt(sepMatch[1], 10);
57
- const b = parseInt(sepMatch[2], 10);
58
- const year = sepMatch[3];
59
- let month;
60
- let day;
61
- if (a > 12 && b <= 12) {
62
- day = a;
63
- month = b;
64
- } else if (b > 12 && a <= 12) {
65
- month = a;
66
- day = b;
67
- } else {
68
- month = a;
69
- day = b;
9
+ // src/state/variableStore.ts
10
+ var VariableStore = class {
11
+ constructor(initial) {
12
+ this.listeners = /* @__PURE__ */ new Set();
13
+ /** Tracks which keys changed in the last mutation — used by scoped notify */
14
+ this.changedKeys = [];
15
+ this.state = { ...initial };
16
+ }
17
+ getState() {
18
+ return this.state;
19
+ }
20
+ get(key) {
21
+ return this.state[key];
22
+ }
23
+ set(key, value) {
24
+ if (this.state[key] === value) return;
25
+ this.state = { ...this.state, [key]: value };
26
+ this.changedKeys = [key];
27
+ this.notify();
28
+ }
29
+ setState(updater) {
30
+ const prev = this.state;
31
+ const next = updater(prev);
32
+ if (next === prev) return;
33
+ this.state = next;
34
+ this.changedKeys = [];
35
+ for (const key of Object.keys(next)) {
36
+ if (next[key] !== prev[key]) this.changedKeys.push(key);
70
37
  }
71
- if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
72
- return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
38
+ for (const key of Object.keys(prev)) {
39
+ if (!(key in next) && prev[key] !== void 0) this.changedKeys.push(key);
73
40
  }
41
+ this.notify();
74
42
  }
75
- const ymdSlash = s.match(/^(\d{4})[/\-.](\d{1,2})[/\-.](\d{1,2})$/);
76
- if (ymdSlash) {
77
- const year = ymdSlash[1];
78
- const month = parseInt(ymdSlash[2], 10);
79
- const day = parseInt(ymdSlash[3], 10);
80
- if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
81
- return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
43
+ /** Batch set multiple variables at once */
44
+ setMany(updates) {
45
+ const changed = [];
46
+ const next = { ...this.state };
47
+ for (const [key, value] of Object.entries(updates)) {
48
+ if (next[key] !== value) {
49
+ next[key] = value;
50
+ changed.push(key);
51
+ }
82
52
  }
53
+ if (changed.length === 0) return;
54
+ this.state = next;
55
+ this.changedKeys = changed;
56
+ this.notify();
83
57
  }
84
- if (/^\d{8}$/.test(s)) {
85
- const a = parseInt(s.slice(0, 2), 10);
86
- const b = parseInt(s.slice(2, 4), 10);
87
- const year = s.slice(4, 8);
88
- let month;
89
- let day;
90
- if (a > 12 && b <= 12) {
91
- day = a;
92
- month = b;
93
- } else {
94
- month = a;
95
- day = b;
96
- }
97
- if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
98
- return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
58
+ /**
59
+ * Subscribe to store changes.
60
+ *
61
+ * @param listener - callback fired when relevant keys change
62
+ * @param scope - optional scope to limit notifications:
63
+ * - `{ keys: ['data.x', 'user.email'] }` — only fire when these exact keys change
64
+ * - `{ prefix: 'answers.' }` only fire when any key starting with this prefix changes
65
+ * - omit or `{}` — fire on every change (global listener)
66
+ */
67
+ subscribe(listener, scope) {
68
+ const entry = {
69
+ listener,
70
+ keys: scope?.keys || null,
71
+ prefix: scope?.prefix || null
72
+ };
73
+ this.listeners.add(entry);
74
+ return () => this.listeners.delete(entry);
75
+ }
76
+ notify() {
77
+ const changed = this.changedKeys;
78
+ for (const entry of this.listeners) {
79
+ if (!entry.keys && !entry.prefix) {
80
+ entry.listener();
81
+ continue;
82
+ }
83
+ if (entry.keys) {
84
+ if (changed.some((k) => entry.keys.includes(k))) {
85
+ entry.listener();
86
+ }
87
+ continue;
88
+ }
89
+ if (entry.prefix) {
90
+ if (changed.some((k) => k.startsWith(entry.prefix))) {
91
+ entry.listener();
92
+ }
93
+ }
99
94
  }
100
95
  }
101
- throw new Error(
102
- `[AppFunnel] Invalid date format: "${input}". Expected a date string like MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, or MMDDYYYY.`
103
- );
104
- }
105
- function toISODateWithFormat(input, format) {
106
- if (!input || !input.trim()) return "";
107
- const s = input.trim();
108
- if (/^\d{4}-\d{2}-\d{2}$/.test(s) || /^\d{4}-\d{2}-\d{2}T/.test(s)) {
109
- return s.slice(0, 10);
96
+ };
97
+
98
+ // src/state/store.ts
99
+ var NAMESPACE_PREFIX = {
100
+ user: "user",
101
+ responses: "responses",
102
+ data: "data"
103
+ };
104
+ var BUILTIN_USER = {
105
+ "user.email": "",
106
+ "user.name": ""
107
+ };
108
+ function defaultFor(type) {
109
+ switch (type) {
110
+ case "string":
111
+ return "";
112
+ case "number":
113
+ return 0;
114
+ case "boolean":
115
+ return false;
116
+ case "stringArray":
117
+ return [];
118
+ default:
119
+ return "";
110
120
  }
111
- const digits = s.replace(/[^\d]/g, "");
112
- if (format === "YYYY-MM-DD") {
113
- const m = s.match(/^(\d{4})[/\-.](\d{1,2})[/\-.](\d{1,2})$/);
114
- if (m) return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
115
- if (digits.length === 8) {
116
- return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
117
- }
121
+ }
122
+ function createFunnelStore(config = {}, sessionValues) {
123
+ const initial = { ...BUILTIN_USER };
124
+ for (const [key, cfg] of Object.entries(config.responses ?? {})) {
125
+ initial[`responses.${key}`] = cfg.default ?? defaultFor(cfg.type);
118
126
  }
119
- if (format === "MM/DD/YYYY") {
120
- const m = s.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
121
- if (m) return `${m[3]}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
122
- if (digits.length === 8) {
123
- return `${digits.slice(4, 8)}-${digits.slice(0, 2)}-${digits.slice(2, 4)}`;
124
- }
127
+ for (const [key, cfg] of Object.entries(config.data ?? {})) {
128
+ initial[`data.${key}`] = cfg.default ?? defaultFor(cfg.type);
125
129
  }
126
- if (format === "DD/MM/YYYY") {
127
- const m = s.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
128
- if (m) return `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
129
- if (digits.length === 8) {
130
- return `${digits.slice(4, 8)}-${digits.slice(2, 4)}-${digits.slice(0, 2)}`;
130
+ if (sessionValues) {
131
+ for (const [key, value] of Object.entries(sessionValues)) {
132
+ if (value !== void 0) initial[key] = value;
131
133
  }
132
134
  }
133
- throw new Error(
134
- `[AppFunnel] Invalid date format: "${input}". Expected format ${format} (e.g. ${{
135
- "MM/DD/YYYY": "03/15/1990 or 03151990",
136
- "DD/MM/YYYY": "15/03/1990 or 15031990",
137
- "YYYY-MM-DD": "1990-03-15 or 19900315"
138
- }[format]}).`
139
- );
135
+ return new VariableStore(initial);
140
136
  }
141
137
 
142
- // src/hooks/useUser.ts
143
- function useUser() {
144
- const { variableStore, tracker } = chunk7BSBNWJ6_cjs.useFunnelContext();
145
- const subscribe = react.useCallback(
146
- (cb) => variableStore.subscribe(cb, { prefix: "user." }),
147
- [variableStore]
148
- );
149
- const getSnapshot = react.useCallback(
150
- () => variableStore.getState(),
151
- [variableStore]
152
- );
153
- const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
154
- return react.useMemo(
155
- () => ({
156
- email: variables["user.email"] || "",
157
- name: variables["user.name"] || "",
158
- stripeCustomerId: variables["user.stripeCustomerId"] || "",
159
- paddleCustomerId: variables["user.paddleCustomerId"] || "",
160
- dateOfBirth: variables["user.dateOfBirth"] || "",
161
- marketingConsent: variables["user.marketingConsent"] === true,
162
- setEmail(email) {
163
- variableStore.set("user.email", email);
164
- },
165
- setName(name) {
166
- variableStore.set("user.name", name);
167
- },
168
- setDateOfBirth(dateOfBirth) {
169
- variableStore.set("user.dateOfBirth", toISODate(dateOfBirth));
138
+ // src/state/context.ts
139
+ var cached = null;
140
+ function extractVersion(ua, regex) {
141
+ return ua.match(regex)?.[1] ?? "0";
142
+ }
143
+ function detect() {
144
+ if (cached) return cached;
145
+ if (typeof window === "undefined") {
146
+ cached = {
147
+ device: {
148
+ type: "desktop",
149
+ isMobile: false,
150
+ isTablet: false,
151
+ screenWidth: 0,
152
+ screenHeight: 0,
153
+ viewportWidth: 0,
154
+ viewportHeight: 0,
155
+ colorDepth: 24,
156
+ pixelRatio: 1
170
157
  },
171
- setMarketingConsent(consent) {
172
- variableStore.set("user.marketingConsent", consent);
173
- if (consent) {
174
- tracker.track("marketing.consent_given");
175
- }
158
+ browser: {
159
+ name: "Unknown",
160
+ version: "0",
161
+ userAgent: "unknown",
162
+ language: "en",
163
+ cookieEnabled: true,
164
+ online: true
176
165
  },
177
- identify(email) {
178
- tracker.identify(email);
179
- }
180
- }),
181
- [variables, variableStore, tracker]
182
- );
166
+ os: { name: "Unknown", timezone: "UTC" },
167
+ locale: { locale: "en", language: "en", region: "", timeZone: "UTC" }
168
+ };
169
+ return cached;
170
+ }
171
+ const ua = navigator.userAgent;
172
+ const l = ua.toLowerCase();
173
+ let name = "Unknown";
174
+ let version = "0";
175
+ if (l.includes("firefox")) {
176
+ name = "Firefox";
177
+ version = extractVersion(ua, /Firefox\/(\d+[\d.]*)/);
178
+ } else if (l.includes("edg")) {
179
+ name = "Edge";
180
+ version = extractVersion(ua, /Edg\/(\d+[\d.]*)/);
181
+ } else if (l.includes("chrome") && !l.includes("edg")) {
182
+ name = "Chrome";
183
+ version = extractVersion(ua, /Chrome\/(\d+[\d.]*)/);
184
+ } else if (l.includes("safari") && !l.includes("chrome")) {
185
+ name = "Safari";
186
+ version = extractVersion(ua, /Version\/(\d+[\d.]*)/);
187
+ }
188
+ let osName = "Unknown";
189
+ if (l.includes("win")) osName = "Windows";
190
+ else if (l.includes("mac")) osName = "macOS";
191
+ else if (l.includes("linux") && !l.includes("android")) osName = "Linux";
192
+ else if (l.includes("android")) osName = "Android";
193
+ else if (/iphone|ipad|ipod/.test(l)) osName = "iOS";
194
+ let type = "desktop";
195
+ if (/mobile|android(?!.*tablet)|iphone|ipod/.test(l)) type = "mobile";
196
+ else if (/tablet|ipad/.test(l)) type = "tablet";
197
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
198
+ const fullLocale = navigator.language || "en";
199
+ const [language = "en", region = ""] = fullLocale.split("-");
200
+ cached = {
201
+ device: {
202
+ type,
203
+ isMobile: type === "mobile",
204
+ isTablet: type === "tablet",
205
+ screenWidth: window.screen?.width || 0,
206
+ screenHeight: window.screen?.height || 0,
207
+ viewportWidth: window.innerWidth || 0,
208
+ viewportHeight: window.innerHeight || 0,
209
+ colorDepth: window.screen?.colorDepth || 24,
210
+ pixelRatio: window.devicePixelRatio || 1
211
+ },
212
+ browser: {
213
+ name,
214
+ version,
215
+ userAgent: ua,
216
+ language: fullLocale,
217
+ cookieEnabled: navigator.cookieEnabled ?? true,
218
+ online: navigator.onLine ?? true
219
+ },
220
+ os: { name: osName, timezone: timeZone },
221
+ locale: { locale: fullLocale, language, region, timeZone }
222
+ };
223
+ return cached;
183
224
  }
184
- function useUserProperty(field) {
185
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
186
- const key = `user.${field}`;
187
- const subscribe = react.useCallback(
188
- (cb) => variableStore.subscribe(cb, { keys: [key] }),
189
- [variableStore, key]
190
- );
191
- const getSnapshot = react.useCallback(
192
- () => variableStore.get(key) || "",
193
- [variableStore, key]
194
- );
195
- const value = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
196
- const setValue = react.useCallback(
197
- (v) => variableStore.set(key, v),
198
- [variableStore, key]
199
- );
200
- return [value, setValue];
225
+ function collectUtm() {
226
+ if (typeof window === "undefined") return {};
227
+ const out = {};
228
+ const params = new URLSearchParams(window.location.search);
229
+ params.forEach((value, key) => {
230
+ if (key.startsWith("utm_")) out[key.slice(4)] = value;
231
+ });
232
+ return out;
201
233
  }
202
- function useDateOfBirth(format = "MM/DD/YYYY") {
203
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
204
- const key = "user.dateOfBirth";
205
- const subscribe = react.useCallback(
206
- (cb) => variableStore.subscribe(cb, { keys: [key] }),
207
- [variableStore]
208
- );
209
- const getSnapshot = react.useCallback(
210
- () => variableStore.get(key) || "",
211
- [variableStore]
212
- );
213
- const value = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
214
- const setValue = react.useCallback(
215
- (v) => variableStore.set(key, toISODateWithFormat(v, format)),
216
- [variableStore, format]
217
- );
218
- return [value, setValue];
234
+ var CLICK_ID_PARAMS = [
235
+ "fbclid",
236
+ "gclid",
237
+ "gbraid",
238
+ "wbraid",
239
+ "msclkid",
240
+ "ttclid",
241
+ "twclid",
242
+ "sccid",
243
+ "li_fat_id",
244
+ "epik",
245
+ "rdt_cid",
246
+ "dclid",
247
+ "irclickid"
248
+ ];
249
+ function collectClickIds() {
250
+ if (typeof window === "undefined") return {};
251
+ const out = {};
252
+ const params = new URLSearchParams(window.location.search);
253
+ for (const key of CLICK_ID_PARAMS) {
254
+ const v = params.get(key);
255
+ if (v) out[key] = v;
256
+ }
257
+ return out;
219
258
  }
220
- function useQueryParams() {
221
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
222
- const subscribe = react.useCallback(
223
- (cb) => variableStore.subscribe(cb, { prefix: "query." }),
224
- [variableStore]
225
- );
226
- const getSnapshot = react.useCallback(
227
- () => variableStore.getState(),
228
- [variableStore]
259
+ function readCookie(name) {
260
+ if (typeof document === "undefined") return void 0;
261
+ const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
262
+ return m ? decodeURIComponent(m[1]) : void 0;
263
+ }
264
+ function buildAcquisition(context) {
265
+ return {
266
+ utm: context.utm,
267
+ clickIds: context.clickIds,
268
+ cookies: { fbp: readCookie("_fbp"), fbc: readCookie("_fbc") },
269
+ referrer: context.session.referrer,
270
+ landingPath: typeof window !== "undefined" ? window.location.pathname : ""
271
+ };
272
+ }
273
+ function buildContext(opts = {}) {
274
+ const raw = detect();
275
+ const now = opts.now ?? 0;
276
+ const referrer = typeof document !== "undefined" ? document.referrer : "";
277
+ const base2 = {
278
+ device: raw.device,
279
+ browser: raw.browser,
280
+ os: raw.os,
281
+ locale: raw.locale,
282
+ utm: collectUtm(),
283
+ clickIds: collectClickIds(),
284
+ page: { key: "", slug: "", index: 0, total: 0, progressPercentage: 0, startedAt: now },
285
+ session: { id: opts.sessionId ?? null, startedAt: now, referrer },
286
+ identity: { customerId: opts.customerId ?? null, visitorId: opts.visitorId ?? null },
287
+ system: {
288
+ funnelId: opts.funnelId ?? "",
289
+ campaignId: opts.campaignId ?? "",
290
+ mode: opts.mode ?? "live",
291
+ now
292
+ }
293
+ };
294
+ if (!opts.overrides) return base2;
295
+ return {
296
+ ...base2,
297
+ ...opts.overrides,
298
+ device: { ...base2.device, ...opts.overrides.device },
299
+ browser: { ...base2.browser, ...opts.overrides.browser },
300
+ os: { ...base2.os, ...opts.overrides.os },
301
+ locale: { ...base2.locale, ...opts.overrides.locale },
302
+ utm: { ...base2.utm, ...opts.overrides.utm },
303
+ clickIds: { ...base2.clickIds, ...opts.overrides.clickIds },
304
+ page: { ...base2.page, ...opts.overrides.page },
305
+ session: { ...base2.session, ...opts.overrides.session },
306
+ identity: { ...base2.identity, ...opts.overrides.identity },
307
+ system: { ...base2.system, ...opts.overrides.system }
308
+ };
309
+ }
310
+ var _eventIdCounter = 0;
311
+ function newEventId() {
312
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
313
+ return `af_${Date.now().toString(36)}_${(_eventIdCounter++).toString(36)}`;
314
+ }
315
+ function createConsoleTracker(opts = {}) {
316
+ const log = (label, ...args) => {
317
+ if (!opts.silent) console.info(`[appfunnel] ${label}`, ...args);
318
+ };
319
+ return {
320
+ track: (event, data) => log(`track ${event}`, data ?? {}),
321
+ identify: (email, eventId) => log("identify", email, eventId),
322
+ setVariables: (vars) => log("vars", vars),
323
+ setAcquisition: (acq) => log("acquisition", acq)
324
+ };
325
+ }
326
+ var TrackerContext = react.createContext(null);
327
+ function TrackerProvider({
328
+ tracker,
329
+ children
330
+ }) {
331
+ return react.createElement(TrackerContext.Provider, { value: tracker }, children);
332
+ }
333
+ function useTrackerRef() {
334
+ return react.useContext(TrackerContext) ?? FALLBACK;
335
+ }
336
+ var FALLBACK = createConsoleTracker({ silent: true });
337
+ function useTracker() {
338
+ const tracker = useTrackerRef();
339
+ const track = react.useCallback(
340
+ (event, data, userData) => tracker.track(event, data, userData),
341
+ [tracker]
229
342
  );
230
- const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
231
- return react.useMemo(() => {
232
- const params = {};
233
- for (const [key, value] of Object.entries(variables)) {
234
- if (key.startsWith("query.") && typeof value === "string") {
235
- params[key.slice(6)] = value;
343
+ return { track };
344
+ }
345
+
346
+ // src/tracking/bus.ts
347
+ var MAX_HISTORY = 250;
348
+ function createBus(accessors, now = () => Date.now()) {
349
+ const listeners = /* @__PURE__ */ new Map();
350
+ const events = [];
351
+ const on = (event, cb) => {
352
+ let set = listeners.get(event);
353
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
354
+ set.add(cb);
355
+ return () => off(event, cb);
356
+ };
357
+ const off = (event, cb) => {
358
+ listeners.get(event)?.delete(cb);
359
+ };
360
+ const emit = (event, data) => {
361
+ events.push({ event, data, ts: now() });
362
+ if (events.length > MAX_HISTORY) events.shift();
363
+ for (const cb of listeners.get(event) ?? []) {
364
+ try {
365
+ cb(data);
366
+ } catch {
367
+ }
368
+ }
369
+ for (const cb of listeners.get("*") ?? []) {
370
+ try {
371
+ cb({ event, data });
372
+ } catch {
236
373
  }
237
374
  }
238
- return params;
239
- }, [variables]);
375
+ };
376
+ return { on, off, events, emit, ...accessors };
240
377
  }
241
- function useQueryParam(key) {
242
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
243
- const prefixedKey = `query.${key}`;
244
- const subscribe = react.useCallback(
245
- (cb) => variableStore.subscribe(cb, { keys: [prefixedKey] }),
246
- [variableStore, prefixedKey]
247
- );
248
- const getSnapshot = react.useCallback(
249
- () => variableStore.get(prefixedKey) || "",
250
- [variableStore, prefixedKey]
251
- );
252
- return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
378
+ function withBus(tracker, bus) {
379
+ return {
380
+ // Spread preserves the original call arity (don't append an undefined userData).
381
+ track: ((...args) => {
382
+ tracker.track(...args);
383
+ bus.emit(args[0], args[1]);
384
+ }),
385
+ identify: (email, eventId) => {
386
+ const id = eventId ?? newEventId();
387
+ tracker.identify(email, id);
388
+ bus.emit("user.registered", { email, eventId: id });
389
+ },
390
+ setVariables: tracker.setVariables ? (v) => tracker.setVariables(v) : void 0,
391
+ // Acquisition is persistence (session + first-touch Customer), not a bus event.
392
+ setAcquisition: tracker.setAcquisition ? (a) => tracker.setAcquisition(a) : void 0
393
+ };
253
394
  }
254
- function useData(key) {
255
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
256
- const prefixedKey = `data.${key}`;
257
- const subscribe = react.useCallback(
258
- (cb) => variableStore.subscribe(cb, { keys: [prefixedKey] }),
259
- [variableStore, prefixedKey]
260
- );
261
- const getSnapshot = react.useCallback(
262
- () => variableStore.get(prefixedKey),
263
- [variableStore, prefixedKey]
264
- );
265
- const value = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
266
- const setValue = react.useCallback(
267
- (v) => variableStore.set(prefixedKey, v),
268
- [variableStore, prefixedKey]
395
+ function attachBus(bus) {
396
+ if (typeof window === "undefined") return () => {
397
+ };
398
+ const { on, off, getVariable, getVariables, getCurrentPageId, getCustomerId, getVisitorId } = bus;
399
+ const api = {
400
+ on,
401
+ off,
402
+ getVariable,
403
+ getVariables,
404
+ getCurrentPageId,
405
+ getCustomerId,
406
+ getVisitorId,
407
+ get events() {
408
+ return bus.events;
409
+ }
410
+ };
411
+ window.appfunnel = api;
412
+ return () => {
413
+ if (window.appfunnel === api) delete window.appfunnel;
414
+ };
415
+ }
416
+ var RTL_LANGS = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "ps", "sd", "dv", "yi"]);
417
+ function isRtl(locale) {
418
+ return RTL_LANGS.has(locale.split("-")[0]);
419
+ }
420
+ var base = (l) => l?.split("-")[0];
421
+ function resolveLocale(config, detected, override) {
422
+ const supported = config?.supported ?? (config?.default ? [config.default] : ["en"]);
423
+ const def = config?.default ?? supported[0] ?? "en";
424
+ const pick = (l) => {
425
+ if (!l) return void 0;
426
+ if (supported.includes(l)) return l;
427
+ return supported.find((s) => base(s) === base(l));
428
+ };
429
+ return pick(override) ?? pick(detected) ?? def;
430
+ }
431
+ function interpolate(str, params) {
432
+ if (!params) return str;
433
+ return str.replace(/\{\{(\w+)\}\}/g, (_, k) => k in params ? String(params[k]) : `{{${k}}}`);
434
+ }
435
+ function lookup(catalog, chain, key) {
436
+ for (const loc of chain) {
437
+ const v = catalog[loc]?.[key];
438
+ if (v !== void 0) return v;
439
+ }
440
+ return void 0;
441
+ }
442
+ function devWarn(message) {
443
+ const env = globalThis.process?.env?.NODE_ENV;
444
+ if (env === "production") return;
445
+ try {
446
+ console.warn(message);
447
+ } catch {
448
+ }
449
+ }
450
+ var LocaleContext = react.createContext(null);
451
+ function LocaleProvider({
452
+ config,
453
+ catalog = {},
454
+ detected,
455
+ override,
456
+ children
457
+ }) {
458
+ const [locale, setLocale] = react.useState(() => resolveLocale(config, detected, override));
459
+ const value = react.useMemo(
460
+ () => ({ locale, setLocale, config, catalog }),
461
+ [locale, config, catalog]
269
462
  );
270
- return [value, setValue];
463
+ return react.createElement(LocaleContext.Provider, { value }, children);
271
464
  }
272
- function useLocale() {
273
- return react.useMemo(() => {
274
- if (typeof navigator === "undefined") {
275
- return {
276
- locale: "en-US",
277
- language: "en",
278
- region: "US",
279
- languages: ["en-US"],
280
- timeZone: "UTC",
281
- is24Hour: false
282
- };
283
- }
284
- const locale = navigator.language || "en-US";
285
- const [language, region] = locale.split("-");
286
- const languages = [...navigator.languages || [locale]];
287
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
288
- const is24Hour = detect24Hour(locale);
289
- return {
290
- locale,
291
- language: language || "en",
292
- region: region?.toUpperCase() || "US",
293
- languages,
294
- timeZone,
295
- is24Hour
465
+ function useActiveLocale() {
466
+ return react.useContext(LocaleContext)?.locale ?? "en";
467
+ }
468
+ function useTranslation() {
469
+ const ctx = react.useContext(LocaleContext);
470
+ if (!ctx) throw new Error("useTranslation must be used inside <FunnelProvider>");
471
+ const { locale, setLocale, config, catalog } = ctx;
472
+ const chain = react.useMemo(() => {
473
+ const def = config?.default;
474
+ const fb = config?.fallback;
475
+ return Array.from(new Set([locale, fb, def].filter(Boolean)));
476
+ }, [locale, config]);
477
+ const t = react.useMemo(() => {
478
+ const fn = ((key, params) => {
479
+ const raw = lookup(catalog, chain, key);
480
+ if (raw === void 0) {
481
+ devWarn(`[appfunnel] missing translation: "${key}" (${locale})`);
482
+ return key;
483
+ }
484
+ return interpolate(raw, params);
485
+ });
486
+ fn.plural = (n, forms, params) => {
487
+ const rule = new Intl.PluralRules(locale).select(n);
488
+ const form = forms[rule] ?? forms.other ?? "";
489
+ return interpolate(form.replace(/#/g, String(n)), params);
296
490
  };
297
- }, []);
491
+ return fn;
492
+ }, [catalog, chain, locale]);
493
+ const fmt = react.useMemo(
494
+ () => ({
495
+ number: (n, opts) => new Intl.NumberFormat(locale, opts).format(n),
496
+ date: (d, opts) => new Intl.DateTimeFormat(locale, opts).format(d),
497
+ percent: (n, opts) => new Intl.NumberFormat(locale, { style: "percent", ...opts }).format(n)
498
+ }),
499
+ [locale]
500
+ );
501
+ return {
502
+ t,
503
+ fmt,
504
+ locale,
505
+ setLocale,
506
+ dir: isRtl(locale) ? "rtl" : "ltr",
507
+ locales: config?.supported ?? [locale]
508
+ };
298
509
  }
299
- function detect24Hour(locale) {
510
+
511
+ // src/commerce/catalog.ts
512
+ var PERIOD_DAYS = {
513
+ day: 1,
514
+ week: 7,
515
+ month: 30,
516
+ year: 365,
517
+ one_time: 0
518
+ };
519
+ function periodDays(interval, count) {
520
+ return PERIOD_DAYS[interval] * count;
521
+ }
522
+ function intervalLabel(interval, count) {
523
+ if (interval === "one_time") return { period: "one-time", periodly: "one-time" };
524
+ if (interval === "month" && count === 3) return { period: "quarter", periodly: "quarterly" };
525
+ if (interval === "month" && count === 6) return { period: "6 months", periodly: "semiannually" };
526
+ if (interval === "week" && count === 2) return { period: "2 weeks", periodly: "biweekly" };
527
+ if (count === 1) {
528
+ const periodly = { day: "daily", week: "weekly", month: "monthly", year: "yearly" }[interval];
529
+ return { period: interval, periodly };
530
+ }
531
+ return { period: `${count} ${interval}s`, periodly: `every ${count} ${interval}s` };
532
+ }
533
+ function resolveProduct(input) {
534
+ const interval = input.interval ?? "one_time";
535
+ const count = input.intervalCount ?? 1;
536
+ const days = periodDays(interval, count);
537
+ const perDayMinor = days > 0 ? input.amount / days : input.amount;
538
+ const label = intervalLabel(interval, count);
539
+ return {
540
+ id: input.id,
541
+ name: input.name ?? input.id,
542
+ displayName: input.displayName ?? input.name ?? input.id,
543
+ provider: input.provider ?? "stripe",
544
+ currency: input.currency,
545
+ priceMinor: input.amount,
546
+ perDayMinor,
547
+ perWeekMinor: perDayMinor * 7,
548
+ perMonthMinor: perDayMinor * 30,
549
+ perYearMinor: perDayMinor * 365,
550
+ period: label.period,
551
+ periodly: label.periodly,
552
+ hasTrial: (input.trialDays ?? 0) > 0,
553
+ trialDays: input.trialDays ?? 0,
554
+ trialMinor: input.trialAmount ?? 0
555
+ };
556
+ }
557
+ function buildCatalog(inputs) {
558
+ return new Map(inputs.map((i) => [i.id, resolveProduct(i)]));
559
+ }
560
+ function formatMoney(minor, currency, locale) {
561
+ const amount = minor / 100;
562
+ const code = currency.toUpperCase();
563
+ let formatted;
300
564
  try {
301
- const formatted = new Intl.DateTimeFormat(locale, { hour: "numeric" }).format(/* @__PURE__ */ new Date());
302
- return !formatted.match(/am|pm/i);
565
+ formatted = new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(amount);
303
566
  } catch {
304
- return false;
567
+ formatted = `${code} ${amount.toFixed(2)}`;
305
568
  }
569
+ return { amount, minor, currency: code, formatted };
306
570
  }
307
- function useTranslation() {
308
- const { i18n } = chunk7BSBNWJ6_cjs.useFunnelContext();
309
- const subscribe = react.useCallback(
310
- (cb) => i18n.subscribe(cb),
311
- [i18n]
571
+ function formatProduct(r, locale) {
572
+ const m = (minor) => formatMoney(minor, r.currency, locale);
573
+ return {
574
+ id: r.id,
575
+ name: r.name,
576
+ displayName: r.displayName,
577
+ provider: r.provider,
578
+ price: m(r.priceMinor),
579
+ period: r.period,
580
+ periodly: r.periodly,
581
+ perDay: m(r.perDayMinor),
582
+ perWeek: m(r.perWeekMinor),
583
+ perMonth: m(r.perMonthMinor),
584
+ perYear: m(r.perYearMinor),
585
+ hasTrial: r.hasTrial,
586
+ trialDays: r.trialDays,
587
+ trialPrice: r.hasTrial ? m(r.trialMinor) : null
588
+ };
589
+ }
590
+ var CatalogContext = react.createContext(null);
591
+ function CatalogProvider({
592
+ catalog,
593
+ children
594
+ }) {
595
+ return react.createElement(CatalogContext.Provider, { value: catalog }, children);
596
+ }
597
+ function useCatalog() {
598
+ return react.useContext(CatalogContext) ?? EMPTY;
599
+ }
600
+ var EMPTY = /* @__PURE__ */ new Map();
601
+ function useProduct(id) {
602
+ const catalog = useCatalog();
603
+ const locale = useActiveLocale();
604
+ const resolved = id ? catalog.get(id) : void 0;
605
+ return react.useMemo(() => resolved ? formatProduct(resolved, locale) : void 0, [resolved, locale]);
606
+ }
607
+ function useProducts() {
608
+ const catalog = useCatalog();
609
+ const locale = useActiveLocale();
610
+ return react.useMemo(
611
+ () => Array.from(catalog.values()).map((r) => formatProduct(r, locale)),
612
+ [catalog, locale]
312
613
  );
313
- const getSnapshot = react.useCallback(
314
- () => i18n.getLocale(),
315
- [i18n]
614
+ }
615
+
616
+ // src/flow/spine.ts
617
+ function readField(s, field) {
618
+ let cur = s;
619
+ for (const part of field.split(".")) {
620
+ if (cur == null) return void 0;
621
+ cur = cur[part];
622
+ }
623
+ return cur;
624
+ }
625
+ function evaluateCondition(cond, s) {
626
+ const val = readField(s, cond.field);
627
+ const { op, value } = cond;
628
+ switch (op) {
629
+ case "eq":
630
+ return val === value;
631
+ case "neq":
632
+ return val !== value;
633
+ case "in":
634
+ return Array.isArray(value) && value.includes(val);
635
+ case "gt":
636
+ return typeof val === "number" && typeof value === "number" && val > value;
637
+ case "gte":
638
+ return typeof val === "number" && typeof value === "number" && val >= value;
639
+ case "lt":
640
+ return typeof val === "number" && typeof value === "number" && val < value;
641
+ case "lte":
642
+ return typeof val === "number" && typeof value === "number" && val <= value;
643
+ case "contains":
644
+ if (typeof val === "string") return val.includes(String(value));
645
+ if (Array.isArray(val)) return val.includes(value);
646
+ return false;
647
+ case "exists":
648
+ return val !== void 0 && val !== null && val !== "";
649
+ case "empty":
650
+ return val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
651
+ default:
652
+ return false;
653
+ }
654
+ }
655
+ function evaluateGate(gate, s) {
656
+ return typeof gate === "function" ? gate(s) : evaluateCondition(gate, s);
657
+ }
658
+ function resolveRoute(routes, s) {
659
+ if (!routes) return void 0;
660
+ for (const route of routes) {
661
+ if (!route.when || evaluateGate(route.when, s)) return route.to;
662
+ }
663
+ return void 0;
664
+ }
665
+ function pageMeta(meta) {
666
+ return meta;
667
+ }
668
+ var TYPE_GUARDS = {
669
+ paywall: { field: "user.email", op: "exists" },
670
+ upsell: { field: "user.email", op: "exists" }
671
+ };
672
+ function entryGuard(meta) {
673
+ return meta?.guard ?? (meta?.type ? TYPE_GUARDS[meta.type] : void 0);
674
+ }
675
+ function definePage(component) {
676
+ return component;
677
+ }
678
+ function nextPage(pages, currentKey, s) {
679
+ const idx = pages.findIndex((p) => p.key === currentKey);
680
+ if (idx === -1) return void 0;
681
+ const routed = resolveRoute(pages[idx].meta?.next, s);
682
+ if (routed) return routed;
683
+ return pages[idx + 1]?.key;
684
+ }
685
+ function outgoingKeys(pages, currentKey) {
686
+ const idx = pages.findIndex((p) => p.key === currentKey);
687
+ if (idx === -1) return [];
688
+ const out = /* @__PURE__ */ new Set();
689
+ for (const route of pages[idx].meta?.next ?? []) out.add(route.to);
690
+ const linear = pages[idx + 1]?.key;
691
+ if (linear) out.add(linear);
692
+ return [...out];
693
+ }
694
+ function expectedPathLength(pages, startKey, s) {
695
+ const seen = /* @__PURE__ */ new Set();
696
+ let key = startKey;
697
+ let count = 0;
698
+ while (key && !seen.has(key)) {
699
+ seen.add(key);
700
+ count++;
701
+ if (pages.find((p) => p.key === key)?.meta?.type === "finish") break;
702
+ key = nextPage(pages, key, s);
703
+ }
704
+ return count;
705
+ }
706
+ function defineFunnel(def) {
707
+ return def;
708
+ }
709
+
710
+ // src/flow/experiments.ts
711
+ function fnv1a(input) {
712
+ let h = 2166136261;
713
+ for (let i = 0; i < input.length; i++) {
714
+ h ^= input.charCodeAt(i);
715
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
716
+ }
717
+ return h >>> 0;
718
+ }
719
+ function hashToUnit(seed) {
720
+ return fnv1a(seed) / 4294967296;
721
+ }
722
+ function pickByWeight(weights, u) {
723
+ const entries = Object.entries(weights).filter(([, w]) => w > 0);
724
+ if (entries.length === 0) return Object.keys(weights)[0] ?? "";
725
+ const total = entries.reduce((sum, [, w]) => sum + w, 0);
726
+ const target = u * total;
727
+ let acc = 0;
728
+ for (const [key, w] of entries) {
729
+ acc += w;
730
+ if (target < acc) return key;
731
+ }
732
+ return entries[entries.length - 1][0];
733
+ }
734
+ function assignVariant(experimentId, seed, weights) {
735
+ return pickByWeight(weights, hashToUnit(`${experimentId}:${seed}`));
736
+ }
737
+ function bucketingSeed(context) {
738
+ return context.identity.customerId ?? context.identity.visitorId ?? null;
739
+ }
740
+ function parseSlotKey(key) {
741
+ const at = key.indexOf("@");
742
+ return at === -1 ? { slot: key } : { slot: key.slice(0, at), variant: key.slice(at + 1) };
743
+ }
744
+ function isVariantKey(key) {
745
+ return parseSlotKey(key).variant !== void 0;
746
+ }
747
+ function weightsOf(exp) {
748
+ const out = {};
749
+ for (const [label, v] of Object.entries(exp.variants)) out[label] = v.weight;
750
+ return out;
751
+ }
752
+ function resolveExperiments(pages, experiments, seed) {
753
+ const assignments = {};
754
+ const render = {};
755
+ const experimentOf = {};
756
+ for (const exp of experiments) {
757
+ const status = exp.status ?? "running";
758
+ let label;
759
+ if (status === "stopped" && exp.winner) label = exp.winner;
760
+ else if (status === "running" && seed) label = assignVariant(exp.id, seed, weightsOf(exp));
761
+ else continue;
762
+ const arm = exp.variants[label];
763
+ if (!arm) continue;
764
+ assignments[exp.id] = label;
765
+ experimentOf[exp.slot] = exp.id;
766
+ if (arm.page !== exp.slot) render[exp.slot] = arm.page;
767
+ }
768
+ const slotOf = {};
769
+ const activeKeys = [];
770
+ for (const p of pages) {
771
+ const { slot, variant } = parseSlotKey(p.key);
772
+ if (variant !== void 0) slotOf[p.key] = slot;
773
+ else activeKeys.push(p.key);
774
+ }
775
+ return { assignments, activeKeys, render, slotOf, experimentOf };
776
+ }
777
+ function validateExperiments(experiments, pages) {
778
+ const errors = [];
779
+ const warnings = [];
780
+ const pageKeys = new Set(pages.map((p) => p.key));
781
+ const seenIds = /* @__PURE__ */ new Set();
782
+ const slotOwner = /* @__PURE__ */ new Map();
783
+ const renderedVariantPages = /* @__PURE__ */ new Set();
784
+ for (const exp of experiments) {
785
+ const err = (code, message) => errors.push({ experimentId: exp.id, code, message });
786
+ const warn = (code, message) => warnings.push({ experimentId: exp.id, code, message });
787
+ if (seenIds.has(exp.id)) err("duplicate_id", `Experiment id "${exp.id}" is used more than once.`);
788
+ seenIds.add(exp.id);
789
+ if (!pageKeys.has(exp.slot)) err("slot_missing", `Slot "${exp.slot}" is not a page in the funnel.`);
790
+ if (isVariantKey(exp.slot)) err("slot_is_variant", `Slot "${exp.slot}" is a variant page; a slot must be a real flow page.`);
791
+ if (slotOwner.has(exp.slot)) {
792
+ err("slot_conflict", `Slot "${exp.slot}" is already targeted by experiment "${slotOwner.get(exp.slot)}".`);
793
+ } else {
794
+ slotOwner.set(exp.slot, exp.id);
795
+ }
796
+ const arms = Object.entries(exp.variants);
797
+ if (arms.length < 2) warn("single_arm", `Experiment "${exp.id}" has fewer than two variants \u2014 nothing to test.`);
798
+ let hasControl = false;
799
+ let totalWeight = 0;
800
+ for (const [label, arm] of arms) {
801
+ if (!pageKeys.has(arm.page)) {
802
+ err("variant_page_missing", `Variant "${label}" references page "${arm.page}", which doesn't exist.`);
803
+ }
804
+ if (arm.page === exp.slot) hasControl = true;
805
+ else if (isVariantKey(arm.page)) {
806
+ renderedVariantPages.add(arm.page);
807
+ if (parseSlotKey(arm.page).slot !== exp.slot) {
808
+ err("variant_slot_mismatch", `Variant "${label}" page "${arm.page}" belongs to a different slot than "${exp.slot}".`);
809
+ }
810
+ }
811
+ if (arm.weight < 0) err("negative_weight", `Variant "${label}" has a negative weight (${arm.weight}).`);
812
+ totalWeight += arm.weight;
813
+ }
814
+ if (!hasControl) err("no_control", `No variant of "${exp.id}" renders the slot "${exp.slot}" itself (the control/baseline).`);
815
+ if (totalWeight <= 0) err("no_traffic", `Experiment "${exp.id}" has no positive weight \u2014 no traffic would enter it.`);
816
+ if (exp.status === "stopped" && exp.winner && !exp.variants[exp.winner]) {
817
+ err("bad_winner", `Stopped experiment "${exp.id}" names winner "${exp.winner}", which isn't one of its variants.`);
818
+ }
819
+ }
820
+ const running = new Set(experiments.filter((e) => (e.status ?? "running") === "running").flatMap(
821
+ (e) => Object.values(e.variants).map((v) => v.page)
822
+ ));
823
+ for (const p of pages) {
824
+ if (isVariantKey(p.key) && !running.has(p.key)) {
825
+ warnings.push({ experimentId: "*", code: "orphan_variant", message: `Variant page "${p.key}" is rendered by no running experiment.` });
826
+ }
827
+ }
828
+ return { ok: errors.length === 0, errors, warnings };
829
+ }
830
+
831
+ // src/flow/flow.tsx
832
+ var NavContext = react.createContext(null);
833
+ var ExperimentContext = react.createContext({});
834
+ function useExperiment(id) {
835
+ return react.useContext(ExperimentContext)[id];
836
+ }
837
+ function pageContextFor(currentKey, slug, index, total, startedAt) {
838
+ const denom = Math.max(total, index + 1);
839
+ return {
840
+ key: currentKey,
841
+ slug,
842
+ index,
843
+ total: denom,
844
+ progressPercentage: denom > 0 ? Math.min(100, Math.round((index + 1) / denom * 100)) : 0,
845
+ startedAt
846
+ };
847
+ }
848
+ function FunnelView({
849
+ pages,
850
+ initialKey,
851
+ layout,
852
+ fallback,
853
+ prefetch: prefetchMode = "auto",
854
+ onNavigate
855
+ }) {
856
+ const funnel = useFunnel();
857
+ const tracker = useTrackerRef();
858
+ const components = react.useMemo(() => {
859
+ const map = /* @__PURE__ */ new Map();
860
+ for (const p of pages) {
861
+ if (p.Component) map.set(p.key, p.Component);
862
+ else if (p.load) {
863
+ const load = p.load;
864
+ map.set(p.key, react.lazy(() => Promise.resolve(load()).then(
865
+ (m) => typeof m === "function" ? { default: m } : m
866
+ )));
867
+ }
868
+ }
869
+ return map;
870
+ }, [pages]);
871
+ const loaders = react.useMemo(
872
+ () => new Map(pages.filter((p) => p.load).map((p) => [p.key, p.load])),
873
+ [pages]
316
874
  );
317
- const locale = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
318
- const t = react.useCallback(
319
- (key, params) => i18n.t(key, params),
320
- [i18n, locale]
321
- // eslint-disable-line react-hooks/exhaustive-deps
875
+ const seed = bucketingSeed(funnel.context);
876
+ const experiments = react.useMemo(
877
+ () => resolveExperiments(pages, funnel.experiments, seed),
878
+ [pages, funnel.experiments, seed]
322
879
  );
323
- const setLocale = react.useCallback(
324
- (l) => i18n.setLocale(l),
325
- [i18n]
880
+ const pageByKey = react.useMemo(() => new Map(pages.map((p) => [p.key, p])), [pages]);
881
+ const flowPages = react.useMemo(
882
+ () => experiments.activeKeys.map((k) => pageByKey.get(k)).filter(Boolean),
883
+ [experiments, pageByKey]
326
884
  );
327
- const availableLocales = i18n.getAvailableLocales();
328
- return { t, locale, setLocale, availableLocales };
329
- }
330
- function useProducts() {
331
- const { products, variableStore, selectProduct: ctxSelect } = chunk7BSBNWJ6_cjs.useFunnelContext();
332
- const subscribe = react.useCallback(
333
- (cb) => variableStore.subscribe(cb, { keys: ["products.selectedProductId"] }),
334
- [variableStore]
885
+ const flow = react.useMemo(
886
+ () => flowPages.map((p) => ({ key: p.key, meta: p.meta })),
887
+ [flowPages]
335
888
  );
336
- const getSnapshot = react.useCallback(
337
- () => variableStore.get("products.selectedProductId"),
338
- [variableStore]
889
+ const [history, setHistory] = react.useState(() => {
890
+ const start = flowPages[0]?.key;
891
+ if (!initialKey || initialKey === start) return [start].filter(Boolean);
892
+ const target = experiments.slotOf[initialKey] ?? initialKey;
893
+ const page2 = flowPages.find((p) => p.key === target);
894
+ const guard = entryGuard(page2?.meta);
895
+ const ok = page2 && (!guard || evaluateGate(guard, funnel.snapshot()));
896
+ return [ok ? target : start].filter(Boolean);
897
+ });
898
+ const currentKey = history[history.length - 1];
899
+ const index = history.length - 1;
900
+ const startKey = history[0];
901
+ const total = react.useMemo(
902
+ () => expectedPathLength(flow, startKey, funnel.snapshot()),
903
+ // currentKey in deps: re-trace as each step's answers settle the branch.
904
+ [flow, startKey, currentKey, funnel]
339
905
  );
340
- const selectedId = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
341
- const selected = products.find((p) => p.id === selectedId) || null;
342
- const select = react.useCallback((productId) => {
343
- ctxSelect(productId);
344
- }, [ctxSelect]);
345
- return { products, selected, select };
346
- }
347
- function useTracking() {
348
- const { tracker } = chunk7BSBNWJ6_cjs.useFunnelContext();
349
- const track = react.useCallback(
350
- (eventName, data) => {
351
- tracker.track(eventName, data);
352
- },
353
- [tracker]
906
+ const slug = pageByKey.get(currentKey)?.meta?.slug ?? currentKey;
907
+ const page = react.useMemo(
908
+ () => pageContextFor(currentKey, slug, index, total, funnel.context.system.now),
909
+ [currentKey, slug, index, total, funnel.context.system.now]
354
910
  );
355
- return { track };
356
- }
357
- var API_BASE_URL = "https://api.appfunnel.net";
358
- var PAYMENT_KEYS = ["payment.loading", "payment.error"];
359
- function usePayment() {
360
- const { variableStore, products, campaignId, tracker, config } = chunk7BSBNWJ6_cjs.useFunnelContext();
361
- const showToasts = !config.settings?.disableToasts;
362
- const subscribe = react.useCallback(
363
- (cb) => variableStore.subscribe(cb, { keys: PAYMENT_KEYS }),
364
- [variableStore]
911
+ funnel.context.page = page;
912
+ const started = react.useRef(false);
913
+ react.useEffect(() => {
914
+ if (started.current) return;
915
+ started.current = true;
916
+ tracker.track("funnel.start", {});
917
+ }, [tracker]);
918
+ react.useEffect(() => {
919
+ if (!currentKey) return;
920
+ tracker.track("page.view", { pageId: currentKey, pageKey: currentKey, isInitial: index === 0, eventId: newEventId() });
921
+ }, [tracker, currentKey, index]);
922
+ react.useEffect(() => {
923
+ if (currentKey && onNavigate) onNavigate(currentKey, slug);
924
+ }, [onNavigate, currentKey, slug]);
925
+ const exposed = react.useRef(/* @__PURE__ */ new Set());
926
+ react.useEffect(() => {
927
+ const expId = experiments.experimentOf[currentKey];
928
+ if (!expId || exposed.current.has(expId)) return;
929
+ exposed.current.add(expId);
930
+ tracker.track("experiment.exposure", { experimentId: expId, variant: experiments.assignments[expId] });
931
+ }, [tracker, currentKey, experiments]);
932
+ const timer = react.useRef(null);
933
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
934
+ const emitExit = react.useCallback(() => {
935
+ const t = timer.current;
936
+ if (!t) return;
937
+ const at = now();
938
+ const hidden = t.hidden + (t.hiddenSince != null ? at - t.hiddenSince : 0);
939
+ const durationMs = Math.round(at - t.enteredAt);
940
+ tracker.track("page.exit", { pageId: t.key, durationMs, activeMs: Math.max(0, Math.round(durationMs - hidden)) });
941
+ }, [tracker]);
942
+ react.useEffect(() => {
943
+ if (typeof document === "undefined") return;
944
+ const onVis = () => {
945
+ const t = timer.current;
946
+ if (!t) return;
947
+ if (document.visibilityState === "hidden") t.hiddenSince = now();
948
+ else if (t.hiddenSince != null) {
949
+ t.hidden += now() - t.hiddenSince;
950
+ t.hiddenSince = null;
951
+ }
952
+ };
953
+ document.addEventListener("visibilitychange", onVis);
954
+ return () => document.removeEventListener("visibilitychange", onVis);
955
+ }, []);
956
+ react.useEffect(() => {
957
+ if (!currentKey) return;
958
+ if (timer.current && timer.current.key !== currentKey) emitExit();
959
+ const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
960
+ timer.current = { key: currentKey, enteredAt: now(), hidden: 0, hiddenSince: hidden ? now() : null };
961
+ }, [currentKey, emitExit]);
962
+ react.useEffect(() => () => emitExit(), [emitExit]);
963
+ const next = react.useCallback(() => {
964
+ const target = nextPage(flow, currentKey, funnel.snapshot());
965
+ const resolved = target ? experiments.slotOf[target] ?? target : target;
966
+ if (resolved) setHistory((h) => [...h, resolved]);
967
+ }, [flow, currentKey, funnel, experiments]);
968
+ const back = react.useCallback(
969
+ () => setHistory((h) => h.length > 1 ? h.slice(0, -1) : h),
970
+ []
365
971
  );
366
- const getSnapshot = react.useCallback(
367
- () => variableStore.getState(),
368
- [variableStore]
972
+ const go = react.useCallback(
973
+ (key) => setHistory((h) => [...h, experiments.slotOf[key] ?? key]),
974
+ [experiments]
369
975
  );
370
- const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
371
- const purchase = react.useCallback(
372
- async (productId, options) => {
373
- console.log("[Purchase] Starting purchase for product:", productId);
374
- if (globalThis.__APPFUNNEL_DEV__) {
375
- console.log(
376
- "[Purchase] Dev mode \u2014 simulating success (500ms delay)"
377
- );
378
- variableStore.set("payment.loading", true);
379
- await new Promise((r) => setTimeout(r, 500));
380
- variableStore.set("payment.error", "");
381
- console.log("[Purchase] Dev mode \u2014 success");
382
- options?.onSuccess?.();
383
- return true;
384
- }
385
- const customerId = variableStore.get(
386
- "user.stripeCustomerId"
387
- );
388
- console.log("[Purchase] Customer ID:", customerId || "(none)");
389
- if (!customerId) {
390
- const msg = "Please complete payment authorization first";
391
- console.error("[Purchase] Failed:", msg);
392
- variableStore.set("payment.error", msg);
393
- if (showToasts) sonner.toast.error(msg);
394
- options?.onError?.(msg);
395
- return false;
396
- }
397
- const product = products.find((p) => p.id === productId);
398
- console.log(
399
- "[Purchase] Product found:",
400
- product ? {
401
- id: product.id,
402
- name: product.name,
403
- stripePriceId: product.stripePriceId
404
- } : "(not found)"
405
- );
406
- if (!product?.stripePriceId) {
407
- const msg = "Product not found or missing Stripe price";
408
- console.error("[Purchase] Failed:", msg);
409
- variableStore.set("payment.error", msg);
410
- if (showToasts) sonner.toast.error(msg);
411
- options?.onError?.(msg);
412
- return false;
413
- }
414
- const trialPeriodDays = product.hasTrial ? product.trialDays : void 0;
415
- const trialChargePriceId = product.paidTrial && product.trialStorePriceId ? product.trialStorePriceId : void 0;
416
- console.log("[Purchase] Trial config:", {
417
- trialPeriodDays,
418
- trialChargePriceId,
419
- hasTrial: product.hasTrial,
420
- paidTrial: product.paidTrial
421
- });
422
- variableStore.set("payment.loading", true);
423
- variableStore.set("payment.error", "");
424
- const requestBody = {
425
- campaignId,
426
- sessionId: tracker.getSessionId(),
427
- stripePriceId: product.stripePriceId,
428
- trialPeriodDays,
429
- trialChargePriceId
430
- };
431
- console.log(
432
- "[Purchase] Sending request:",
433
- `${API_BASE_URL}/campaign/${campaignId}/stripe/purchase`,
434
- requestBody
435
- );
436
- try {
437
- const response = await fetch(
438
- `${API_BASE_URL}/campaign/${campaignId}/stripe/purchase`,
439
- {
440
- method: "POST",
441
- headers: { "Content-Type": "application/json" },
442
- body: JSON.stringify(requestBody)
443
- }
444
- );
445
- console.log("[Purchase] Response status:", response.status);
446
- let result = await response.json();
447
- console.log("[Purchase] Response body:", result);
448
- if (!result.success && result.requiresAction) {
449
- console.log(
450
- "[Purchase] 3DS authentication required, loading Stripe..."
451
- );
452
- const stripeInstance = await stripeJs.loadStripe(
453
- result.publishableKey
454
- );
455
- if (!stripeInstance) {
456
- const msg = "Failed to load payment processor";
457
- console.error("[Purchase] Failed:", msg);
458
- variableStore.set("payment.error", msg);
459
- if (showToasts) sonner.toast.error(msg);
460
- options?.onError?.(msg);
461
- return false;
462
- }
463
- console.log(
464
- "[Purchase] Confirming card payment with 3DS..."
465
- );
466
- const { error: confirmError, paymentIntent: confirmedPi } = await stripeInstance.confirmCardPayment(
467
- result.clientSecret,
468
- {
469
- payment_method: result.paymentMethodId
470
- }
471
- );
472
- if (confirmError || !confirmedPi || confirmedPi.status !== "succeeded") {
473
- const msg = confirmError?.message || "Payment authentication failed";
474
- console.error("[Purchase] 3DS failed:", msg);
475
- variableStore.set("payment.error", msg);
476
- if (showToasts) sonner.toast.error(msg);
477
- options?.onError?.(msg);
478
- return false;
479
- }
480
- console.log(
481
- "[Purchase] 3DS succeeded, retrying purchase with confirmed PI:",
482
- confirmedPi.id
483
- );
484
- const retryResponse = await fetch(
485
- `${API_BASE_URL}/campaign/${campaignId}/stripe/purchase`,
486
- {
487
- method: "POST",
488
- headers: { "Content-Type": "application/json" },
489
- body: JSON.stringify({
490
- campaignId,
491
- sessionId: tracker.getSessionId(),
492
- stripePriceId: product.stripePriceId,
493
- trialPeriodDays,
494
- trialChargePriceId,
495
- onSessionPiId: confirmedPi.id
496
- })
497
- }
498
- );
499
- result = await retryResponse.json();
500
- console.log("[Purchase] Retry response:", result);
501
- }
502
- if (result.success) {
503
- console.log("[Purchase] Success! Type:", result.type);
504
- variableStore.set("payment.error", "");
505
- if (result.type === "validate_only") {
506
- console.log(
507
- "[Purchase] Validate-only \u2014 setting card variables"
508
- );
509
- if (result.card) {
510
- variableStore.setMany({
511
- "card.last4": result.card.last4,
512
- "card.brand": result.card.brand,
513
- "card.expMonth": result.card.expMonth,
514
- "card.expYear": result.card.expYear,
515
- "card.funding": result.card.funding
516
- });
517
- }
518
- if (result.eventId) {
519
- console.log(
520
- "[Purchase] Tracking purchase.complete (validate_only), eventId:",
521
- result.eventId
522
- );
523
- tracker.track("purchase.complete", {
524
- eventId: result.eventId,
525
- amount: product.rawPrice ? product.rawPrice / 100 : 0,
526
- currency: product.currencyCode || "USD"
527
- });
528
- }
529
- } else if (result.type === "one_time") {
530
- console.log(
531
- "[Purchase] One-time charge \u2014 paymentIntentId:",
532
- result.paymentIntentId
533
- );
534
- variableStore.set(
535
- "stripe.paymentIntentId",
536
- result.paymentIntentId
537
- );
538
- variableStore.set("payment.status", result.status);
539
- if (result.eventId) {
540
- console.log(
541
- "[Purchase] Tracking purchase.complete (one_time), eventId:",
542
- result.eventId
543
- );
544
- tracker.track("purchase.complete", {
545
- eventId: result.eventId,
546
- amount: product.rawPrice ? product.rawPrice / 100 : 0,
547
- currency: product.currencyCode || "USD"
548
- });
549
- }
550
- } else {
551
- console.log(
552
- "[Purchase] Subscription \u2014 subscriptionId:",
553
- result.subscriptionId,
554
- "status:",
555
- result.status
556
- );
557
- variableStore.set(
558
- "stripe.subscriptionId",
559
- result.subscriptionId
560
- );
561
- variableStore.set("subscription.status", result.status);
562
- if (result.trialCharge) {
563
- console.log(
564
- "[Purchase] Tracking trial charge purchase.complete"
565
- );
566
- tracker.track("purchase.complete", {
567
- eventId: result.eventIds?.trialCharge,
568
- amount: result.trialCharge.amount / 100,
569
- currency: "USD"
570
- });
571
- }
572
- if (result.eventIds?.subscription) {
573
- console.log(
574
- "[Purchase] Tracking subscription.created, eventId:",
575
- result.eventIds.subscription
576
- );
577
- tracker.track("subscription.created", {
578
- eventId: result.eventIds.subscription,
579
- subscriptionId: result.subscriptionId,
580
- status: result.status
581
- });
582
- }
583
- if (result.eventIds?.firstPeriod) {
584
- console.log(
585
- "[Purchase] Tracking first-period purchase.complete, eventId:",
586
- result.eventIds.firstPeriod
587
- );
588
- tracker.track("purchase.complete", {
589
- eventId: result.eventIds.firstPeriod,
590
- amount: product.rawPrice ? product.rawPrice / 100 : 0,
591
- currency: product.currencyCode || "USD"
592
- });
593
- }
594
- }
595
- console.log("[Purchase] Calling onSuccess callback");
596
- options?.onSuccess?.();
597
- return true;
598
- } else {
599
- const msg = result.error || "Purchase failed";
600
- console.error("[Purchase] Failed:", msg);
601
- variableStore.set("payment.error", msg);
602
- if (showToasts) sonner.toast.error(msg);
603
- options?.onError?.(msg);
604
- return false;
605
- }
606
- } catch (err) {
607
- const msg = err instanceof Error ? err.message : "Purchase failed";
608
- console.error("[Purchase] Exception:", err);
609
- variableStore.set("payment.error", msg);
610
- if (showToasts) sonner.toast.error(msg);
611
- options?.onError?.(msg);
612
- return false;
613
- } finally {
614
- console.log("[Purchase] Done \u2014 setting loading to false");
615
- variableStore.set("payment.loading", false);
616
- }
976
+ const prefetched = react.useRef(/* @__PURE__ */ new Set());
977
+ const prefetch = react.useCallback(
978
+ (key) => {
979
+ const target = experiments.render[key] ?? key;
980
+ const load = loaders.get(target);
981
+ if (!load || prefetched.current.has(target)) return;
982
+ prefetched.current.add(target);
983
+ load().catch(() => prefetched.current.delete(target));
617
984
  },
618
- [variableStore, products, campaignId, tracker, showToasts]
985
+ [loaders, experiments]
986
+ );
987
+ const nextCandidates = react.useMemo(
988
+ () => [...new Set(outgoingKeys(flow, currentKey).map((k) => experiments.render[k] ?? k))],
989
+ [flow, currentKey, experiments]
990
+ );
991
+ react.useEffect(() => {
992
+ if (prefetchMode !== "auto") return;
993
+ for (const key of nextCandidates) prefetch(key);
994
+ }, [prefetchMode, nextCandidates, prefetch]);
995
+ const value = {
996
+ page,
997
+ next,
998
+ back,
999
+ go,
1000
+ prefetch,
1001
+ nextCandidates,
1002
+ canGoBack: history.length > 1
1003
+ };
1004
+ const renderKey = experiments.render[currentKey] ?? currentKey;
1005
+ const Current = components.get(renderKey);
1006
+ const pageEl = react.createElement(react.Suspense, { fallback: fallback ?? null }, Current ? react.createElement(Current) : null);
1007
+ const content = layout ? react.createElement(layout, null, pageEl) : pageEl;
1008
+ return react.createElement(
1009
+ ExperimentContext.Provider,
1010
+ { value: experiments.assignments },
1011
+ react.createElement(NavContext.Provider, { value }, content)
619
1012
  );
1013
+ }
1014
+ function useNavigation() {
1015
+ const ctx = react.useContext(NavContext);
1016
+ if (!ctx) throw new Error("useNavigation must be used inside <FunnelView>");
1017
+ return ctx;
1018
+ }
1019
+ function usePage() {
1020
+ return useNavigation().page;
1021
+ }
1022
+ var symModalId = /* @__PURE__ */ Symbol("AppFunnelModalId");
1023
+ var initialState = {};
1024
+ var ModalStateContext = react.createContext(initialState);
1025
+ var ModalIdContext = react.createContext(null);
1026
+ var REGISTRY = {};
1027
+ var ALREADY_MOUNTED = {};
1028
+ var modalCallbacks = {};
1029
+ var hideCallbacks = {};
1030
+ var uid = 0;
1031
+ var getUid = () => `_af_modal_${uid++}`;
1032
+ var dispatch = () => {
1033
+ throw new Error("No modal dispatch \u2014 render inside <FunnelProvider> (which mounts the modal runtime).");
1034
+ };
1035
+ var modalReducer = (state = initialState, action) => {
1036
+ const { modalId, args, flags } = action.payload;
1037
+ switch (action.type) {
1038
+ case "show":
1039
+ return {
1040
+ ...state,
1041
+ [modalId]: {
1042
+ ...state[modalId],
1043
+ id: modalId,
1044
+ args,
1045
+ // Mount first, then flip visible on the next effect → enter transition.
1046
+ visible: !!ALREADY_MOUNTED[modalId],
1047
+ delayVisible: !ALREADY_MOUNTED[modalId]
1048
+ }
1049
+ };
1050
+ case "hide":
1051
+ return state[modalId] ? { ...state, [modalId]: { ...state[modalId], visible: false } } : state;
1052
+ case "remove": {
1053
+ const next = { ...state };
1054
+ delete next[modalId];
1055
+ return next;
1056
+ }
1057
+ case "set-flags":
1058
+ return { ...state, [modalId]: { ...state[modalId], ...flags } };
1059
+ default:
1060
+ return state;
1061
+ }
1062
+ };
1063
+ function idOf(modal) {
1064
+ if (typeof modal === "string") return modal;
1065
+ const m = modal;
1066
+ if (!m[symModalId]) m[symModalId] = getUid();
1067
+ return m[symModalId];
1068
+ }
1069
+ function registerModal(id, comp, props) {
1070
+ if (!REGISTRY[id]) REGISTRY[id] = { comp, props };
1071
+ else REGISTRY[id].props = props;
1072
+ }
1073
+ function unregisterModal(id) {
1074
+ delete REGISTRY[id];
1075
+ }
1076
+ function showModal(modal, args) {
1077
+ const id = idOf(modal);
1078
+ if (typeof modal !== "string" && !REGISTRY[id]) registerModal(id, modal);
1079
+ dispatch({ type: "show", payload: { modalId: id, args } });
1080
+ if (!modalCallbacks[id]) {
1081
+ let res;
1082
+ let rej;
1083
+ const promise = new Promise((resolve, reject) => {
1084
+ res = resolve;
1085
+ rej = reject;
1086
+ });
1087
+ modalCallbacks[id] = { resolve: res, reject: rej, promise };
1088
+ }
1089
+ return modalCallbacks[id].promise;
1090
+ }
1091
+ function hideModal(modal) {
1092
+ const id = idOf(modal);
1093
+ dispatch({ type: "hide", payload: { modalId: id } });
1094
+ delete modalCallbacks[id];
1095
+ if (!hideCallbacks[id]) {
1096
+ let res;
1097
+ let rej;
1098
+ const promise = new Promise((resolve, reject) => {
1099
+ res = resolve;
1100
+ rej = reject;
1101
+ });
1102
+ hideCallbacks[id] = { resolve: res, reject: rej, promise };
1103
+ }
1104
+ return hideCallbacks[id].promise;
1105
+ }
1106
+ function removeModal(modal) {
1107
+ const id = idOf(modal);
1108
+ dispatch({ type: "remove", payload: { modalId: id } });
1109
+ delete modalCallbacks[id];
1110
+ delete hideCallbacks[id];
1111
+ }
1112
+ function setFlags(id, flags) {
1113
+ dispatch({ type: "set-flags", payload: { modalId: id, flags } });
1114
+ }
1115
+ function useModal(modal, args) {
1116
+ const modals = react.useContext(ModalStateContext);
1117
+ const contextId = react.useContext(ModalIdContext);
1118
+ const isComponent = !!modal && typeof modal !== "string";
1119
+ const id = modal ? idOf(modal) : contextId;
1120
+ if (!id) throw new Error("useModal: no modal id (call inside a defineModal component, or pass an id/component).");
1121
+ react.useEffect(() => {
1122
+ if (isComponent && !REGISTRY[id]) registerModal(id, modal, args);
1123
+ }, [isComponent, id, modal, args]);
1124
+ const info = modals[id];
1125
+ const show = react.useCallback((a) => showModal(id, a), [id]);
1126
+ const hide = react.useCallback(() => hideModal(id), [id]);
1127
+ const remove = react.useCallback(() => removeModal(id), [id]);
1128
+ const resolve = react.useCallback((v) => {
1129
+ modalCallbacks[id]?.resolve(v);
1130
+ delete modalCallbacks[id];
1131
+ }, [id]);
1132
+ const reject = react.useCallback((v) => {
1133
+ modalCallbacks[id]?.reject(v);
1134
+ delete modalCallbacks[id];
1135
+ }, [id]);
1136
+ const resolveHide = react.useCallback((v) => {
1137
+ hideCallbacks[id]?.resolve(v);
1138
+ delete hideCallbacks[id];
1139
+ }, [id]);
620
1140
  return react.useMemo(
621
1141
  () => ({
622
- loading: !!variables["payment.loading"],
623
- error: variables["payment.error"] || null,
624
- purchase
1142
+ id,
1143
+ args: info?.args,
1144
+ visible: !!info?.visible,
1145
+ keepMounted: !!info?.keepMounted,
1146
+ show,
1147
+ hide,
1148
+ remove,
1149
+ resolve,
1150
+ reject,
1151
+ resolveHide
625
1152
  }),
626
- [variables, purchase]
1153
+ [id, info?.args, info?.visible, info?.keepMounted, show, hide, remove, resolve, reject, resolveHide]
627
1154
  );
628
1155
  }
629
- var DEVICE_KEYS = [
630
- "os.name",
631
- "os.timezone",
632
- "device.type",
633
- "device.isMobile",
634
- "device.isTablet",
635
- "device.screenWidth",
636
- "device.screenHeight",
637
- "device.viewportWidth",
638
- "device.viewportHeight",
639
- "device.colorDepth",
640
- "device.pixelRatio",
641
- "browser.name",
642
- "browser.version",
643
- "browser.userAgent",
644
- "browser.cookieEnabled",
645
- "browser.online",
646
- "browser.language"
647
- ];
648
- function useDeviceInfo() {
649
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
650
- const subscribe = react.useCallback(
651
- (cb) => variableStore.subscribe(cb, { keys: DEVICE_KEYS }),
652
- [variableStore]
1156
+ function defineModal(Comp) {
1157
+ return function Wrapped({ id, defaultVisible, keepMounted, ...props }) {
1158
+ const { args, show } = useModal(id);
1159
+ const modals = react.useContext(ModalStateContext);
1160
+ const shouldMount = !!modals[id];
1161
+ react.useEffect(() => {
1162
+ if (defaultVisible) show();
1163
+ ALREADY_MOUNTED[id] = true;
1164
+ return () => {
1165
+ delete ALREADY_MOUNTED[id];
1166
+ };
1167
+ }, [id, show, defaultVisible]);
1168
+ react.useEffect(() => {
1169
+ if (keepMounted) setFlags(id, { keepMounted: true });
1170
+ }, [id, keepMounted]);
1171
+ const delayVisible = modals[id]?.delayVisible;
1172
+ react.useEffect(() => {
1173
+ if (delayVisible) show(args);
1174
+ }, [delayVisible, args, show]);
1175
+ if (!shouldMount) return null;
1176
+ return react.createElement(
1177
+ ModalIdContext.Provider,
1178
+ { value: id },
1179
+ react.createElement(Comp, { ...props, ...args })
1180
+ );
1181
+ };
1182
+ }
1183
+ function ModalRoot() {
1184
+ const modals = react.useContext(ModalStateContext);
1185
+ const toRender = Object.keys(modals).filter((id) => REGISTRY[id]).map((id) => ({ id, ...REGISTRY[id] }));
1186
+ return react.createElement(
1187
+ "div",
1188
+ { "data-appfunnel-modal-root": "" },
1189
+ ...toRender.map((t) => react.createElement(t.comp, { key: t.id, id: t.id, ...t.props }))
653
1190
  );
654
- const getSnapshot = react.useCallback(
655
- () => variableStore.getState(),
656
- [variableStore]
1191
+ }
1192
+ function ModalRuntime({ children }) {
1193
+ const [state, localDispatch] = react.useReducer(modalReducer, initialState);
1194
+ dispatch = localDispatch;
1195
+ return react.createElement(
1196
+ ModalStateContext.Provider,
1197
+ { value: state },
1198
+ children,
1199
+ react.createElement(ModalRoot, null)
657
1200
  );
658
- const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
659
- return react.useMemo(() => ({
660
- os: {
661
- name: variables["os.name"] || "",
662
- timezone: variables["os.timezone"] || ""
663
- },
664
- device: {
665
- type: variables["device.type"] || "desktop",
666
- isMobile: !!variables["device.isMobile"],
667
- isTablet: !!variables["device.isTablet"],
668
- screenWidth: variables["device.screenWidth"] || 0,
669
- screenHeight: variables["device.screenHeight"] || 0,
670
- viewportWidth: variables["device.viewportWidth"] || 0,
671
- viewportHeight: variables["device.viewportHeight"] || 0,
672
- colorDepth: variables["device.colorDepth"] || 24,
673
- pixelRatio: variables["device.pixelRatio"] || 1
674
- },
675
- browser: {
676
- name: variables["browser.name"] || "",
677
- version: variables["browser.version"] || "",
678
- userAgent: variables["browser.userAgent"] || "",
679
- cookieEnabled: variables["browser.cookieEnabled"] !== false,
680
- online: variables["browser.online"] !== false,
681
- language: variables["browser.language"] || "en"
682
- }
683
- }), [variables]);
684
1201
  }
685
- function useSafeArea() {
686
- const [insets, setInsets] = react.useState({ top: 0, right: 0, bottom: 0, left: 0 });
687
- react.useEffect(() => {
688
- if (typeof window === "undefined") return;
689
- const el = document.createElement("div");
690
- el.style.cssText = [
691
- "position:fixed",
692
- "top:env(safe-area-inset-top,0px)",
693
- "right:env(safe-area-inset-right,0px)",
694
- "bottom:env(safe-area-inset-bottom,0px)",
695
- "left:env(safe-area-inset-left,0px)",
696
- "pointer-events:none",
697
- "visibility:hidden",
698
- "z-index:-1"
699
- ].join(";");
700
- document.body.appendChild(el);
701
- function read() {
702
- const style = getComputedStyle(el);
703
- setInsets({
704
- top: parseFloat(style.top) || 0,
705
- right: parseFloat(style.right) || 0,
706
- bottom: parseFloat(style.bottom) || 0,
707
- left: parseFloat(style.left) || 0
708
- });
1202
+
1203
+ // src/modals/sheet.tsx
1204
+ function useModalControl() {
1205
+ const m = useModal();
1206
+ return {
1207
+ open: m.visible,
1208
+ onClose: () => {
1209
+ m.hide();
1210
+ m.resolveHide();
1211
+ if (!m.keepMounted) m.remove();
709
1212
  }
710
- read();
711
- const observer = new ResizeObserver(read);
712
- observer.observe(el);
713
- window.addEventListener("resize", read);
714
- return () => {
715
- observer.disconnect();
716
- window.removeEventListener("resize", read);
717
- el.remove();
718
- };
719
- }, []);
720
- return insets;
1213
+ };
721
1214
  }
722
- function useKeyboard() {
723
- const [state, setState] = react.useState({ isOpen: false, height: 0 });
724
- const timeoutRef = react.useRef();
1215
+ function OverlayShell({
1216
+ open,
1217
+ onClose,
1218
+ className,
1219
+ backdropClassName,
1220
+ dismissOnBackdrop = true,
1221
+ ariaLabel,
1222
+ container,
1223
+ panel,
1224
+ children
1225
+ }) {
725
1226
  react.useEffect(() => {
726
- if (typeof window === "undefined") return;
727
- if ("virtualKeyboard" in navigator) {
728
- const vk = navigator.virtualKeyboard;
729
- vk.overlaysContent = true;
730
- const handler = () => {
731
- const h = vk.boundingRect.height;
732
- setState((prev) => {
733
- if (prev.height === h && prev.isOpen === h > 0) return prev;
734
- return { isOpen: h > 0, height: h };
735
- });
736
- };
737
- vk.addEventListener("geometrychange", handler);
738
- handler();
739
- return () => vk.removeEventListener("geometrychange", handler);
740
- }
741
- const vv = window.visualViewport;
742
- if (!vv) return;
743
- let layoutHeight = window.innerHeight;
744
- function compute() {
745
- const kbHeight = Math.max(0, layoutHeight - vv.height);
746
- const h = kbHeight > 40 ? Math.round(kbHeight) : 0;
747
- setState((prev) => {
748
- if (prev.height === h && prev.isOpen === h > 0) return prev;
749
- return { isOpen: h > 0, height: h };
750
- });
751
- }
752
- function onViewportResize() {
753
- compute();
754
- clearTimeout(timeoutRef.current);
755
- timeoutRef.current = setTimeout(compute, 1e3);
756
- }
757
- function onWindowResize() {
758
- layoutHeight = window.innerHeight;
759
- compute();
760
- }
761
- vv.addEventListener("resize", onViewportResize);
762
- window.addEventListener("resize", onWindowResize);
763
- window.addEventListener("orientationchange", onWindowResize);
764
- compute();
1227
+ if (!open || typeof document === "undefined") return;
1228
+ const onKey = (e) => {
1229
+ if (e.key === "Escape") onClose();
1230
+ };
1231
+ const prev = document.body.style.overflow;
1232
+ document.body.style.overflow = "hidden";
1233
+ document.addEventListener("keydown", onKey);
765
1234
  return () => {
766
- clearTimeout(timeoutRef.current);
767
- vv.removeEventListener("resize", onViewportResize);
768
- window.removeEventListener("resize", onWindowResize);
769
- window.removeEventListener("orientationchange", onWindowResize);
1235
+ document.body.style.overflow = prev;
1236
+ document.removeEventListener("keydown", onKey);
770
1237
  };
771
- }, []);
772
- return state;
773
- }
774
- var PAGE_KEYS = [
775
- "page.currentId",
776
- "page.currentIndex",
777
- "page.current",
778
- "page.total",
779
- "page.progressPercentage",
780
- "page.startedAt"
781
- ];
782
- function usePageData() {
783
- const { variableStore } = chunk7BSBNWJ6_cjs.useFunnelContext();
784
- const subscribe = react.useCallback(
785
- (cb) => variableStore.subscribe(cb, { keys: PAGE_KEYS }),
786
- [variableStore]
787
- );
788
- const getSnapshot = react.useCallback(
789
- () => variableStore.getState(),
790
- [variableStore]
1238
+ }, [open, onClose]);
1239
+ if (!open || typeof document === "undefined") return null;
1240
+ const overlay = react.createElement(
1241
+ "div",
1242
+ {
1243
+ role: "dialog",
1244
+ "aria-modal": true,
1245
+ "aria-label": ariaLabel,
1246
+ "data-appfunnel-overlay": "",
1247
+ style: { position: "fixed", inset: 0, zIndex: 1e3, display: "flex", ...container }
1248
+ },
1249
+ react.createElement("div", {
1250
+ "data-appfunnel-overlay-backdrop": "",
1251
+ className: backdropClassName,
1252
+ onClick: dismissOnBackdrop ? onClose : void 0,
1253
+ style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" }
1254
+ }),
1255
+ react.createElement(
1256
+ "div",
1257
+ { "data-appfunnel-overlay-panel": "", className, style: { position: "relative", background: "#fff", ...panel } },
1258
+ children
1259
+ )
791
1260
  );
792
- const variables = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
793
- return react.useMemo(() => ({
794
- currentId: variables["page.currentId"] || "",
795
- currentIndex: variables["page.currentIndex"] || 0,
796
- current: variables["page.current"] || 1,
797
- total: variables["page.total"] || 0,
798
- progressPercentage: variables["page.progressPercentage"] || 0,
799
- startedAt: variables["page.startedAt"] || 0
800
- }), [variables]);
1261
+ return reactDom.createPortal(overlay, document.body);
1262
+ }
1263
+ var SHEET_LAYOUT = {
1264
+ bottom: { container: { justifyContent: "flex-end", alignItems: "stretch" }, panel: { borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 16, maxHeight: "90vh", overflow: "auto" } },
1265
+ top: { container: { justifyContent: "flex-start", alignItems: "stretch" }, panel: { borderBottomLeftRadius: 16, borderBottomRightRadius: 16, padding: 16, maxHeight: "90vh", overflow: "auto" } },
1266
+ left: { container: { justifyContent: "flex-start", alignItems: "stretch" }, panel: { height: "100%", width: "min(420px, 90vw)", padding: 16, overflow: "auto" } },
1267
+ right: { container: { justifyContent: "flex-end", alignItems: "stretch" }, panel: { height: "100%", width: "min(420px, 90vw)", padding: 16, overflow: "auto" } }
1268
+ };
1269
+ function Sheet(props) {
1270
+ const control = useModalControl();
1271
+ const layout = SHEET_LAYOUT[props.side ?? "bottom"];
1272
+ return react.createElement(OverlayShell, {
1273
+ ...control,
1274
+ className: props.className,
1275
+ backdropClassName: props.backdropClassName,
1276
+ dismissOnBackdrop: props.dismissOnBackdrop,
1277
+ ariaLabel: props.ariaLabel,
1278
+ container: layout.container,
1279
+ panel: layout.panel,
1280
+ children: props.children
1281
+ });
1282
+ }
1283
+ function Modal(props) {
1284
+ const control = useModalControl();
1285
+ return react.createElement(OverlayShell, {
1286
+ ...control,
1287
+ className: props.className,
1288
+ backdropClassName: props.backdropClassName,
1289
+ dismissOnBackdrop: props.dismissOnBackdrop,
1290
+ ariaLabel: props.ariaLabel,
1291
+ container: { justifyContent: "center", alignItems: "center" },
1292
+ panel: { borderRadius: 16, padding: 24, width: "min(480px, 92vw)", maxHeight: "90vh", overflow: "auto" },
1293
+ children: props.children
1294
+ });
801
1295
  }
802
1296
 
803
- // src/hooks/useFunnel.ts
804
- function useFunnel() {
805
- const { funnelId, campaignId, tracker } = chunk7BSBNWJ6_cjs.useFunnelContext();
806
- return {
807
- funnelId,
808
- campaignId,
809
- sessionId: tracker.getSessionId(),
810
- variables: useVariables(),
811
- user: useUser(),
812
- responses: chunk7D7VJBOV_cjs.useResponses(),
813
- queryParams: useQueryParams(),
814
- navigation: chunk7D7VJBOV_cjs.useNavigation(),
815
- products: useProducts(),
816
- tracking: useTracking(),
817
- payment: usePayment()
818
- };
1297
+ // src/commerce/checkout.tsx
1298
+ var INLINE_SURFACES = /* @__PURE__ */ new Set([
1299
+ "express",
1300
+ "element",
1301
+ "embedded"
1302
+ ]);
1303
+ function isInlineSurface(surface) {
1304
+ return INLINE_SURFACES.has(surface);
819
1305
  }
820
- var FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
821
- var inputStyle = (focused) => ({
822
- width: "100%",
823
- padding: "12px",
824
- border: `1px solid ${focused ? "#666" : "#e0e0e0"}`,
825
- borderRadius: "6px",
826
- fontSize: "14px",
827
- fontFamily: FONT,
828
- outline: "none",
829
- backgroundColor: "#fff",
830
- boxSizing: "border-box",
831
- transition: "border-color 0.15s"
832
- });
833
- var labelStyle = {
834
- display: "block",
835
- fontSize: "14px",
836
- fontWeight: 500,
837
- color: "#333",
838
- marginBottom: "6px",
839
- fontFamily: FONT
1306
+ var ON = { purchase: true, wallets: true, offSession: "reliable" };
1307
+ var MANAGED = { purchase: true, wallets: true, offSession: "conditional" };
1308
+ var ALL_UPSELLS = ["subscription", "one-time", "upgrade"];
1309
+ var PROVIDER_PROFILES = {
1310
+ // PSP; you hold the PaymentMethod and fire your own off-session PaymentIntent on
1311
+ // the Elements surfaces (reliable). **Managed** Checkout (`embedded`/`redirect`)
1312
+ // upsells too, but as a direct charge against Stripe's vaulted card → conditional.
1313
+ // No provider "popup" surface.
1314
+ stripe: {
1315
+ isMoR: false,
1316
+ offSessionUpsells: ALL_UPSELLS,
1317
+ tokenModel: "merchant",
1318
+ orchestrator: false,
1319
+ surfaces: { express: ON, element: ON, sheet: ON, embedded: MANAGED, redirect: MANAGED }
1320
+ },
1321
+ // Merchant-of-Record; card entry is always Paddle's frame → no own element/sheet/
1322
+ // express. Off-session via subscription one-time charge / collection_mode, but
1323
+ // **no 2nd concurrent subscription** (upgrade + one-time only), provider-initiated.
1324
+ paddle: {
1325
+ isMoR: true,
1326
+ offSessionUpsells: ["one-time", "upgrade"],
1327
+ tokenModel: "provider",
1328
+ orchestrator: false,
1329
+ surfaces: { embedded: ON, popup: ON, redirect: ON }
1330
+ },
1331
+ // Merchant-of-Record; iframe-only (no raw card field). Off-session works
1332
+ // (provider-initiated, async via webhooks). `sheet` = our chrome hosting their embed.
1333
+ whop: {
1334
+ isMoR: true,
1335
+ offSessionUpsells: ALL_UPSELLS,
1336
+ tokenModel: "provider",
1337
+ orchestrator: false,
1338
+ surfaces: { embedded: ON, redirect: ON, sheet: ON }
1339
+ },
1340
+ // Pure orchestrator (PSP-side); Headless renders express/element/sheet/embedded.
1341
+ // No hosted redirect page, no popup checkout. Off-session is PSP-conditional.
1342
+ primer: {
1343
+ isMoR: false,
1344
+ offSessionUpsells: ALL_UPSELLS,
1345
+ tokenModel: "merchant",
1346
+ orchestrator: true,
1347
+ surfaces: { express: ON, element: ON, sheet: ON, embedded: ON }
1348
+ },
1349
+ // PSP **and** orchestrator (own acquiring + connectors routing across Stripe/Adyen/
1350
+ // PayPal). One Payment Form (element/sheet/embedded) + hosted page + wallet button.
1351
+ // Strong native off-session (Recurring API). No popup.
1352
+ solidgate: {
1353
+ isMoR: false,
1354
+ offSessionUpsells: ALL_UPSELLS,
1355
+ tokenModel: "merchant",
1356
+ orchestrator: true,
1357
+ surfaces: { express: ON, element: ON, sheet: ON, embedded: ON, redirect: ON }
1358
+ }
840
1359
  };
841
- function CardBrandBadges() {
842
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "4px", alignItems: "center" }, children: [
843
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "10px", fontWeight: 700, color: "#1a1f71", background: "#fff", border: "1px solid #e0e0e0", borderRadius: "3px", padding: "2px 4px", fontFamily: FONT }, children: "VISA" }),
844
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "10px", fontWeight: 700, color: "#eb001b", background: "#fff", border: "1px solid #e0e0e0", borderRadius: "3px", padding: "2px 4px", fontFamily: FONT }, children: "MC" }),
845
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "10px", fontWeight: 700, color: "#ff6000", background: "#fff", border: "1px solid #e0e0e0", borderRadius: "3px", padding: "2px 4px", fontFamily: FONT }, children: "DISC" })
846
- ] });
847
- }
848
- function CvcIcon() {
849
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "16", viewBox: "0 0 20 16", fill: "none", style: { opacity: 0.4 }, children: [
850
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0.5", y: "0.5", width: "19", height: "15", rx: "2", stroke: "#888" }),
851
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0", y: "4", width: "20", height: "3", fill: "#888" }),
852
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "10", width: "8", height: "2", rx: "1", fill: "#ccc" }),
853
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: "14", y: "12", fontSize: "6", fill: "#888", fontFamily: FONT, children: "123" })
854
- ] });
855
- }
856
- function DemoElementsForm({ onReady }) {
857
- const [cardNumber, setCardNumber] = react.useState("");
858
- const [expiry, setExpiry] = react.useState("");
859
- const [cvc, setCvc] = react.useState("");
860
- const [country, setCountry] = react.useState("Denmark");
861
- const [focusedField, setFocusedField] = react.useState(null);
862
- const readyFired = react.useRef(false);
863
- react.useEffect(() => {
864
- if (!readyFired.current) {
865
- readyFired.current = true;
866
- onReady?.();
867
- }
868
- }, [onReady]);
869
- const formatCardNumber = (val) => {
870
- const digits = val.replace(/\D/g, "").slice(0, 16);
871
- return digits.replace(/(.{4})/g, "$1 ").trim();
872
- };
873
- const formatExpiry = (val) => {
874
- const digits = val.replace(/\D/g, "").slice(0, 4);
875
- if (digits.length > 2) return digits.slice(0, 2) + " / " + digits.slice(2);
876
- return digits;
877
- };
878
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontFamily: FONT }, children: [
879
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "14px" }, children: [
880
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Card number" }),
881
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
882
- /* @__PURE__ */ jsxRuntime.jsx(
883
- "input",
884
- {
885
- type: "text",
886
- value: cardNumber,
887
- onChange: (e) => setCardNumber(formatCardNumber(e.target.value)),
888
- onFocus: () => setFocusedField("card"),
889
- onBlur: () => setFocusedField(null),
890
- placeholder: "1234 1234 1234 1234",
891
- style: inputStyle(focusedField === "card")
892
- }
893
- ),
894
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", right: "12px", top: "50%", transform: "translateY(-50%)" }, children: /* @__PURE__ */ jsxRuntime.jsx(CardBrandBadges, {}) })
895
- ] })
896
- ] }),
897
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px", marginBottom: "14px" }, children: [
898
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
899
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Expiry date" }),
900
- /* @__PURE__ */ jsxRuntime.jsx(
901
- "input",
902
- {
903
- type: "text",
904
- value: expiry,
905
- onChange: (e) => setExpiry(formatExpiry(e.target.value)),
906
- onFocus: () => setFocusedField("expiry"),
907
- onBlur: () => setFocusedField(null),
908
- placeholder: "MM / YY",
909
- style: inputStyle(focusedField === "expiry")
910
- }
911
- )
912
- ] }),
913
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
914
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Security code" }),
915
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
916
- /* @__PURE__ */ jsxRuntime.jsx(
917
- "input",
918
- {
919
- type: "text",
920
- value: cvc,
921
- onChange: (e) => setCvc(e.target.value.replace(/\D/g, "").slice(0, 4)),
922
- onFocus: () => setFocusedField("cvc"),
923
- onBlur: () => setFocusedField(null),
924
- placeholder: "CVC",
925
- style: inputStyle(focusedField === "cvc")
926
- }
927
- ),
928
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", right: "12px", top: "50%", transform: "translateY(-50%)" }, children: /* @__PURE__ */ jsxRuntime.jsx(CvcIcon, {}) })
929
- ] })
930
- ] })
931
- ] }),
932
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "14px" }, children: [
933
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: labelStyle, children: "Country" }),
934
- /* @__PURE__ */ jsxRuntime.jsxs(
935
- "select",
936
- {
937
- value: country,
938
- onChange: (e) => setCountry(e.target.value),
939
- onFocus: () => setFocusedField("country"),
940
- onBlur: () => setFocusedField(null),
941
- style: {
942
- ...inputStyle(focusedField === "country"),
943
- appearance: "none",
944
- backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E")`,
945
- backgroundRepeat: "no-repeat",
946
- backgroundPosition: "right 12px center",
947
- paddingRight: "32px"
948
- },
949
- children: [
950
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Australia" }),
951
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Austria" }),
952
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Belgium" }),
953
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Brazil" }),
954
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Canada" }),
955
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Denmark" }),
956
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Finland" }),
957
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "France" }),
958
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Germany" }),
959
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Ireland" }),
960
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Italy" }),
961
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Japan" }),
962
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Netherlands" }),
963
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "New Zealand" }),
964
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Norway" }),
965
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Poland" }),
966
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Portugal" }),
967
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Spain" }),
968
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Sweden" }),
969
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Switzerland" }),
970
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "United Kingdom" }),
971
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "United States" })
972
- ]
973
- }
974
- )
975
- ] }),
976
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "12px", color: "#6b7280", lineHeight: 1.5, margin: 0, fontFamily: FONT }, children: "By providing your card information, you allow the merchant to charge your card for future payments in accordance with their terms." })
977
- ] });
978
- }
979
- function DemoEmbeddedForm({ product, onReady }) {
980
- const readyFired = react.useRef(false);
981
- const [email, setEmail] = react.useState("");
982
- const [cardNumber, setCardNumber] = react.useState("");
983
- const [expiry, setExpiry] = react.useState("");
984
- const [cvc, setCvc] = react.useState("");
985
- const [name, setName] = react.useState("");
986
- const [country, setCountry] = react.useState("Denmark");
987
- const [focusedField, setFocusedField] = react.useState(null);
988
- react.useEffect(() => {
989
- if (!readyFired.current) {
990
- readyFired.current = true;
991
- onReady?.();
992
- }
993
- }, [onReady]);
994
- const formatCardNumber = (val) => {
995
- const digits = val.replace(/\D/g, "").slice(0, 16);
996
- return digits.replace(/(.{4})/g, "$1 ").trim();
997
- };
998
- const formatExpiry = (val) => {
999
- const digits = val.replace(/\D/g, "").slice(0, 4);
1000
- if (digits.length > 2) return digits.slice(0, 2) + " / " + digits.slice(2);
1001
- return digits;
1360
+ function surfacesFor(provider) {
1361
+ return Object.keys(PROVIDER_PROFILES[provider]?.surfaces ?? {});
1362
+ }
1363
+ function isMerchantOfRecord(provider) {
1364
+ return PROVIDER_PROFILES[provider]?.isMoR ?? false;
1365
+ }
1366
+ function isOrchestrator(provider) {
1367
+ return PROVIDER_PROFILES[provider]?.orchestrator ?? false;
1368
+ }
1369
+ function validateCheckout(provider, surface) {
1370
+ const cap = PROVIDER_PROFILES[provider]?.surfaces[surface];
1371
+ if (!cap) return { ok: false, reason: `${provider} has no '${surface}' surface` };
1372
+ return cap.purchase ? { ok: true } : { ok: false, reason: `'${surface}' can't take a purchase on ${provider}` };
1373
+ }
1374
+ function validateUpsell(provider, paywallSurface, kind = "subscription") {
1375
+ const profile = PROVIDER_PROFILES[provider];
1376
+ if (!profile) return { ok: false, reason: `unknown provider '${provider}'` };
1377
+ if (!profile.offSessionUpsells.includes(kind)) {
1378
+ const can = profile.offSessionUpsells.length ? ` (it supports: ${profile.offSessionUpsells.join(", ")})` : "";
1379
+ return { ok: false, reason: `${provider} can't charge a '${kind}' upsell off-session${can}` };
1380
+ }
1381
+ const cap = profile.surfaces[paywallSurface];
1382
+ if (!cap) return { ok: false, reason: `${provider} has no '${paywallSurface}' surface to upsell after` };
1383
+ if (cap.offSession === "none") {
1384
+ return { ok: false, reason: `a purchase on '${paywallSurface}' retains no chargeable method for an upsell` };
1385
+ }
1386
+ if (cap.offSession === "conditional") {
1387
+ return { ok: true, note: `off-session upsell after ${provider} '${paywallSurface}' (managed checkout) may decline \u2014 prefer element/sheet/express` };
1388
+ }
1389
+ return { ok: true };
1390
+ }
1391
+ function checkoutError(category, message, extra = {}) {
1392
+ const retryable = extra.retryable ?? (category === "authentication_required" || category === "requires_payment_method" || category === "processing_error");
1393
+ return { category, message, declineCode: extra.declineCode, retryable };
1394
+ }
1395
+ function createMockDriver(provider = "stripe", options = {}) {
1396
+ const success = options.result ?? { ok: true, amountMinor: 0, currency: "USD" };
1397
+ const offError = typeof options.failOffSession === "object" ? options.failOffSession : checkoutError("authentication_required", "off-session charge declined (mock)");
1398
+ return {
1399
+ provider,
1400
+ start: (req) => {
1401
+ if (options.failOffSession && !req.surface) return Promise.resolve({ ok: false, error: offError });
1402
+ return Promise.resolve(success);
1403
+ },
1404
+ renderInline: (req, cb) => react.createElement(
1405
+ "button",
1406
+ {
1407
+ type: "button",
1408
+ "data-mock-surface": req.surface,
1409
+ onClick: () => success.ok ? cb.onSuccess(success) : cb.onError(success.error ?? checkoutError("processing_error", "failed (mock)"))
1410
+ },
1411
+ `Pay (mock ${req.surface})`
1412
+ )
1002
1413
  };
1003
- const embeddedInputStyle = (focused) => ({
1004
- width: "100%",
1005
- padding: "10px 12px",
1006
- border: "none",
1007
- borderBottom: `1px solid ${focused ? "#666" : "#e0e0e0"}`,
1008
- fontSize: "14px",
1009
- fontFamily: FONT,
1010
- outline: "none",
1011
- backgroundColor: "#fff",
1012
- boxSizing: "border-box"
1013
- });
1014
- const displayPrice = product?.hasTrial ? product.trialPrice : product?.price;
1015
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontFamily: FONT, border: "1px solid #e0e0e0", borderRadius: "12px", overflow: "hidden", background: "#fff" }, children: [
1016
- product && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "24px 20px", borderBottom: "1px solid #e0e0e0" }, children: [
1017
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: "14px", color: "#666", marginBottom: "4px" }, children: [
1018
- "Pay ",
1019
- product.displayName || product.name
1020
- ] }),
1021
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: "28px", fontWeight: 700, color: "#333", marginBottom: "4px" }, children: [
1022
- product.currencyCode?.toUpperCase(),
1023
- displayPrice
1024
- ] }),
1025
- product.hasTrial && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: "13px", color: "#666" }, children: [
1026
- "Then ",
1027
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { textDecoration: "underline" }, children: [
1028
- product.currencyCode?.toUpperCase(),
1029
- product.price
1030
- ] }),
1031
- " every ",
1032
- product.period
1033
- ] }),
1034
- /* @__PURE__ */ jsxRuntime.jsx(
1035
- "button",
1036
- {
1037
- type: "button",
1038
- style: {
1039
- marginTop: "12px",
1040
- padding: "6px 14px",
1041
- borderRadius: "6px",
1042
- border: "1px solid #e0e0e0",
1043
- backgroundColor: "#fff",
1044
- color: "#333",
1045
- fontSize: "13px",
1046
- cursor: "default",
1047
- fontFamily: FONT
1048
- },
1049
- children: "View details \u25BE"
1050
- }
1051
- )
1052
- ] }),
1053
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "20px" }, children: [
1054
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "16px" }, children: [
1055
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: { ...labelStyle, fontSize: "13px", color: "#666" }, children: "Email" }),
1056
- /* @__PURE__ */ jsxRuntime.jsx(
1057
- "input",
1058
- {
1059
- type: "email",
1060
- value: email,
1061
- onChange: (e) => setEmail(e.target.value),
1062
- onFocus: () => setFocusedField("email"),
1063
- onBlur: () => setFocusedField(null),
1064
- placeholder: "you@example.com",
1065
- style: embeddedInputStyle(focusedField === "email")
1066
- }
1067
- )
1068
- ] }),
1069
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "16px" }, children: [
1070
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: { ...labelStyle, fontSize: "13px", color: "#666" }, children: "Card information" }),
1071
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { border: "1px solid #e0e0e0", borderRadius: "6px", overflow: "hidden" }, children: [
1072
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1073
- /* @__PURE__ */ jsxRuntime.jsx(
1074
- "input",
1075
- {
1076
- type: "text",
1077
- value: cardNumber,
1078
- onChange: (e) => setCardNumber(formatCardNumber(e.target.value)),
1079
- onFocus: () => setFocusedField("card"),
1080
- onBlur: () => setFocusedField(null),
1081
- placeholder: "1234 1234 1234 1234",
1082
- style: { ...embeddedInputStyle(focusedField === "card"), padding: "12px" }
1083
- }
1084
- ),
1085
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", right: "12px", top: "50%", transform: "translateY(-50%)" }, children: /* @__PURE__ */ jsxRuntime.jsx(CardBrandBadges, {}) })
1086
- ] }),
1087
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr" }, children: [
1088
- /* @__PURE__ */ jsxRuntime.jsx(
1089
- "input",
1090
- {
1091
- type: "text",
1092
- value: expiry,
1093
- onChange: (e) => setExpiry(formatExpiry(e.target.value)),
1094
- onFocus: () => setFocusedField("expiry"),
1095
- onBlur: () => setFocusedField(null),
1096
- placeholder: "MM / YY",
1097
- style: { ...embeddedInputStyle(focusedField === "expiry"), borderRight: "1px solid #e0e0e0", padding: "12px" }
1098
- }
1099
- ),
1100
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1101
- /* @__PURE__ */ jsxRuntime.jsx(
1102
- "input",
1103
- {
1104
- type: "text",
1105
- value: cvc,
1106
- onChange: (e) => setCvc(e.target.value.replace(/\D/g, "").slice(0, 4)),
1107
- onFocus: () => setFocusedField("cvc"),
1108
- onBlur: () => setFocusedField(null),
1109
- placeholder: "CVC",
1110
- style: { ...embeddedInputStyle(focusedField === "cvc"), padding: "12px" }
1111
- }
1112
- ),
1113
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", right: "12px", top: "50%", transform: "translateY(-50%)" }, children: /* @__PURE__ */ jsxRuntime.jsx(CvcIcon, {}) })
1114
- ] })
1115
- ] })
1116
- ] })
1117
- ] }),
1118
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "16px" }, children: [
1119
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: { ...labelStyle, fontSize: "13px", color: "#666" }, children: "Cardholder name" }),
1120
- /* @__PURE__ */ jsxRuntime.jsx(
1121
- "input",
1122
- {
1123
- type: "text",
1124
- value: name,
1125
- onChange: (e) => setName(e.target.value),
1126
- onFocus: () => setFocusedField("name"),
1127
- onBlur: () => setFocusedField(null),
1128
- placeholder: "Full name on card",
1129
- style: inputStyle(focusedField === "name")
1130
- }
1131
- )
1132
- ] }),
1133
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "20px" }, children: [
1134
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: { ...labelStyle, fontSize: "13px", color: "#666" }, children: "Country or region" }),
1135
- /* @__PURE__ */ jsxRuntime.jsxs(
1136
- "select",
1137
- {
1138
- value: country,
1139
- onChange: (e) => setCountry(e.target.value),
1140
- onFocus: () => setFocusedField("country"),
1141
- onBlur: () => setFocusedField(null),
1142
- style: {
1143
- ...inputStyle(focusedField === "country"),
1144
- appearance: "none",
1145
- backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E")`,
1146
- backgroundRepeat: "no-repeat",
1147
- backgroundPosition: "right 12px center",
1148
- paddingRight: "32px"
1149
- },
1150
- children: [
1151
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Australia" }),
1152
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Austria" }),
1153
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Belgium" }),
1154
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Brazil" }),
1155
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Canada" }),
1156
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Denmark" }),
1157
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Finland" }),
1158
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "France" }),
1159
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Germany" }),
1160
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Ireland" }),
1161
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Italy" }),
1162
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Japan" }),
1163
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Netherlands" }),
1164
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "New Zealand" }),
1165
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Norway" }),
1166
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Poland" }),
1167
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Portugal" }),
1168
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Spain" }),
1169
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Sweden" }),
1170
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "Switzerland" }),
1171
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "United Kingdom" }),
1172
- /* @__PURE__ */ jsxRuntime.jsx("option", { children: "United States" })
1173
- ]
1174
- }
1175
- )
1176
- ] }),
1177
- /* @__PURE__ */ jsxRuntime.jsx(
1178
- "button",
1179
- {
1180
- type: "button",
1181
- disabled: true,
1182
- style: {
1183
- width: "100%",
1184
- padding: "14px",
1185
- borderRadius: "6px",
1186
- border: "none",
1187
- backgroundColor: "#0570de",
1188
- color: "#fff",
1189
- fontSize: "16px",
1190
- fontWeight: 600,
1191
- cursor: "default",
1192
- fontFamily: FONT,
1193
- opacity: 0.7
1194
- },
1195
- children: "Subscribe"
1196
- }
1197
- ),
1198
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", marginTop: "12px", fontSize: "12px", color: "#aaa" }, children: [
1199
- "Powered by ",
1200
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 700, letterSpacing: "-0.3px" }, children: "stripe" })
1201
- ] })
1202
- ] })
1203
- ] });
1204
- }
1205
- var DemoStripePaymentForm = react.forwardRef(
1206
- function DemoStripePaymentForm2({
1207
- productId,
1208
- mode = "checkout",
1209
- variant = "elements",
1210
- onSuccess,
1211
- onError,
1212
- onReady,
1213
- className
1214
- }, ref) {
1215
- const { variableStore, tracker, products } = chunk7BSBNWJ6_cjs.useFunnelContext();
1216
- const validateOnly = mode === "validate-only";
1217
- const product = react.useMemo(() => {
1218
- if (productId) return products.find((p) => p.id === productId) || null;
1219
- const selectedId = variableStore.get("products.selectedProductId");
1220
- return products.find((p) => p.id === selectedId) || null;
1221
- }, [productId, products, variableStore]);
1222
- const hasFiredStart = react.useRef(false);
1223
- react.useEffect(() => {
1224
- if (hasFiredStart.current) return;
1225
- hasFiredStart.current = true;
1226
- variableStore.set("payment.loading", false);
1227
- tracker.track("checkout.start", {
1228
- productId: product?.id,
1229
- priceId: product?.stripePriceId || product?.storePriceId,
1230
- productName: product?.displayName
1414
+ }
1415
+ var DriverContext = react.createContext(null);
1416
+ function CheckoutDriverProvider({
1417
+ driver,
1418
+ children
1419
+ }) {
1420
+ return react.createElement(DriverContext.Provider, { value: driver }, children);
1421
+ }
1422
+ function useDriver() {
1423
+ return react.useContext(DriverContext) ?? FALLBACK_DRIVER;
1424
+ }
1425
+ var FALLBACK_DRIVER = createMockDriver();
1426
+ function useCheckout(opts = {}) {
1427
+ const driver = useDriver();
1428
+ const tracker = useTrackerRef();
1429
+ const nav = useNavigation();
1430
+ const [status, setStatus] = react.useState("idle");
1431
+ const [error, setError] = react.useState(null);
1432
+ const intent = opts.intent ?? "purchase";
1433
+ const kind = opts.kind;
1434
+ const advance = opts.advanceOnSuccess ?? true;
1435
+ const succeed = react.useCallback(
1436
+ (result) => {
1437
+ setStatus("success");
1438
+ tracker.track("purchase.complete", {
1439
+ amount: result.amountMinor != null ? result.amountMinor / 100 : void 0,
1440
+ currency: result.currency,
1441
+ eventId: result.eventId
1442
+ // server-minted (browser↔CAPI dedup); undefined if none
1231
1443
  });
1232
- }, [product, tracker, variableStore]);
1233
- const handleSubmit = react.useCallback(async () => {
1234
- variableStore.set("payment.loading", true);
1235
- await new Promise((r) => setTimeout(r, 800));
1236
- try {
1237
- tracker.track("checkout.payment_added");
1238
- if (validateOnly) {
1239
- variableStore.setMany({
1240
- "card.last4": "4242",
1241
- "card.brand": "visa",
1242
- "card.expMonth": 12,
1243
- "card.expYear": 2030,
1244
- "card.funding": "credit",
1245
- "payment.error": ""
1246
- });
1247
- onSuccess?.();
1248
- return;
1249
- }
1250
- variableStore.set("payment.error", "");
1251
- tracker.track("purchase.complete", {
1252
- amount: product?.rawPrice ? product.rawPrice / 100 : 0,
1253
- currency: product?.currencyCode || "USD"
1254
- });
1255
- onSuccess?.();
1256
- } catch (err) {
1257
- const msg = err instanceof Error ? err.message : "An error occurred";
1258
- variableStore.set("payment.error", msg);
1259
- onError?.(msg);
1260
- } finally {
1261
- variableStore.set("payment.loading", false);
1262
- }
1263
- }, [validateOnly, variableStore, tracker, product, onSuccess, onError]);
1264
- react.useImperativeHandle(ref, () => ({ submit: handleSubmit }), [handleSubmit]);
1265
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: variant === "embedded" ? /* @__PURE__ */ jsxRuntime.jsx(DemoEmbeddedForm, { product, onReady }) : /* @__PURE__ */ jsxRuntime.jsx(DemoElementsForm, { onReady }) });
1266
- }
1267
- );
1268
- var API_BASE_URL2 = "https://api.appfunnel.net";
1269
- var isDevMode = () => typeof globalThis !== "undefined" && !!globalThis.__APPFUNNEL_DEV__;
1270
- var InnerPaymentForm = react.forwardRef(
1271
- function InnerPaymentForm2({ paymentMode, validateOnly, onSuccess, onError, onReady, layout }, ref) {
1272
- const stripe = reactStripeJs.useStripe();
1273
- const elements = reactStripeJs.useElements();
1274
- const [error, setError] = react.useState(null);
1275
- const { variableStore, campaignId, tracker, products, config } = chunk7BSBNWJ6_cjs.useFunnelContext();
1276
- const showToasts = !config.settings?.disableToasts;
1277
- const readyFired = react.useRef(false);
1278
- react.useEffect(() => {
1279
- if (stripe && elements && !readyFired.current) {
1280
- readyFired.current = true;
1281
- onReady?.();
1282
- }
1283
- }, [stripe, elements, onReady]);
1284
- const handleSubmit = react.useCallback(async () => {
1285
- if (!stripe || !elements) {
1286
- const msg = "Stripe not loaded";
1287
- setError(msg);
1288
- onError?.(msg);
1289
- return;
1290
- }
1444
+ opts.onSuccess?.(result);
1445
+ if (advance) nav.next();
1446
+ },
1447
+ [tracker, nav, advance, opts]
1448
+ );
1449
+ const fail = react.useCallback(
1450
+ (err, product) => {
1451
+ setStatus(err.category === "authentication_required" ? "requires_action" : "error");
1452
+ setError(err);
1453
+ tracker.track("checkout.failed", {
1454
+ category: err.category,
1455
+ declineCode: err.declineCode,
1456
+ productId: product,
1457
+ intent
1458
+ });
1459
+ opts.onError?.(err);
1460
+ },
1461
+ [tracker, intent, opts]
1462
+ );
1463
+ const begin = react.useCallback(
1464
+ (product, surface) => {
1465
+ setStatus("loading");
1291
1466
  setError(null);
1292
- variableStore.set("payment.loading", true);
1467
+ tracker.track("checkout.start", { productId: product, surface });
1468
+ },
1469
+ [tracker]
1470
+ );
1471
+ const paymentAdded = react.useCallback(() => tracker.track("checkout.payment_added", {}), [tracker]);
1472
+ const open = react.useCallback(
1473
+ async (product, surface) => {
1474
+ begin(product, surface);
1293
1475
  try {
1294
- const confirmFn = paymentMode === "setup" ? stripe.confirmSetup : stripe.confirmPayment;
1295
- const confirmResult = await confirmFn({
1296
- elements,
1297
- redirect: "if_required",
1298
- confirmParams: { return_url: window.location.href }
1299
- });
1300
- if (confirmResult.error) {
1301
- const msg = confirmResult.error.message || "Payment failed";
1302
- setError(msg);
1303
- variableStore.set("payment.error", msg);
1304
- if (showToasts) sonner.toast.error(msg);
1305
- onError?.(msg);
1306
- return;
1307
- }
1308
- tracker.track("checkout.payment_added");
1309
- if (validateOnly) {
1310
- const piId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent : void 0;
1311
- if (!piId?.id) {
1312
- const msg = "PaymentIntent not found after confirmation";
1313
- setError(msg);
1314
- variableStore.set("payment.error", msg);
1315
- if (showToasts) sonner.toast.error(msg);
1316
- onError?.(msg);
1317
- return;
1318
- }
1319
- const response2 = await fetch(
1320
- `${API_BASE_URL2}/campaign/${campaignId}/stripe/validate-card`,
1321
- {
1322
- method: "POST",
1323
- headers: { "Content-Type": "application/json" },
1324
- body: JSON.stringify({
1325
- campaignId,
1326
- sessionId: tracker.getSessionId(),
1327
- paymentIntentId: piId.id
1328
- })
1329
- }
1330
- );
1331
- const result2 = await response2.json();
1332
- if (!result2.success) {
1333
- const msg = result2.error || "Card validation failed";
1334
- setError(msg);
1335
- variableStore.set("payment.error", msg);
1336
- if (showToasts) sonner.toast.error(msg);
1337
- onError?.(msg);
1338
- return;
1339
- }
1340
- variableStore.setMany({
1341
- "card.last4": result2.card.last4,
1342
- "card.brand": result2.card.brand,
1343
- "card.expMonth": result2.card.expMonth,
1344
- "card.expYear": result2.card.expYear,
1345
- "card.funding": result2.card.funding,
1346
- "payment.error": ""
1347
- });
1348
- onSuccess?.();
1349
- return;
1350
- }
1351
- const paymentIntentId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent?.id : void 0;
1352
- const selectedId = variableStore.get("products.selectedProductId");
1353
- const product = products.find((p) => p.id === selectedId);
1354
- if (!product?.stripePriceId) {
1355
- const msg = "No product selected or missing Stripe price";
1356
- setError(msg);
1357
- variableStore.set("payment.error", msg);
1358
- if (showToasts) sonner.toast.error(msg);
1359
- onError?.(msg);
1360
- return;
1361
- }
1362
- const response = await fetch(
1363
- `${API_BASE_URL2}/campaign/${campaignId}/stripe/purchase`,
1364
- {
1365
- method: "POST",
1366
- headers: { "Content-Type": "application/json" },
1367
- body: JSON.stringify({
1368
- campaignId,
1369
- sessionId: tracker.getSessionId(),
1370
- stripePriceId: product.stripePriceId,
1371
- trialPeriodDays: product.hasTrial ? product.trialDays : void 0,
1372
- onSessionPiId: paymentMode === "payment" ? paymentIntentId : void 0
1373
- })
1374
- }
1375
- );
1376
- const result = await response.json();
1377
- if (result.success) {
1378
- variableStore.set("payment.error", "");
1379
- if (result.eventId || result.eventIds?.firstPeriod) {
1380
- tracker.track("purchase.complete", {
1381
- amount: product.rawPrice ? product.rawPrice / 100 : 0,
1382
- currency: product.currencyCode || "USD"
1383
- });
1384
- }
1385
- onSuccess?.();
1386
- } else {
1387
- const msg = result.error || "Failed to process payment";
1388
- setError(msg);
1389
- variableStore.set("payment.error", msg);
1390
- if (showToasts) sonner.toast.error(msg);
1391
- onError?.(msg);
1476
+ const result = await driver.start({ product, intent, surface, kind });
1477
+ if (result.ok) succeed(result);
1478
+ else fail(result.error ?? checkoutError("unknown", "Checkout failed"), product);
1479
+ return result;
1480
+ } catch (e) {
1481
+ const err = checkoutError("processing_error", e instanceof Error ? e.message : "Checkout failed");
1482
+ fail(err, product);
1483
+ return { ok: false, error: err };
1484
+ }
1485
+ },
1486
+ [driver, intent, kind, begin, succeed, fail]
1487
+ );
1488
+ const callbacksFor = react.useCallback(
1489
+ (product, surface) => ({
1490
+ onStart: () => begin(product, surface),
1491
+ onSuccess: (result) => succeed(result),
1492
+ onError: (err) => fail(err, product),
1493
+ onPaymentAdded: () => paymentAdded()
1494
+ }),
1495
+ [begin, succeed, fail, paymentAdded]
1496
+ );
1497
+ const reset = react.useCallback(() => {
1498
+ setStatus("idle");
1499
+ setError(null);
1500
+ }, []);
1501
+ return { open, status, error, isLoading: status === "loading", reset, callbacksFor };
1502
+ }
1503
+ var CheckoutSheet = defineModal(({ product, surface, intent, onSuccess, onError }) => {
1504
+ const driver = useDriver();
1505
+ const modal = useModal();
1506
+ if (!driver.renderInline) return null;
1507
+ return react.createElement(Sheet, {
1508
+ children: driver.renderInline(
1509
+ { product, intent, surface },
1510
+ {
1511
+ onSuccess: (r) => {
1512
+ modal.hide();
1513
+ onSuccess(r);
1514
+ },
1515
+ onError: (e) => {
1516
+ modal.hide();
1517
+ onError(e);
1392
1518
  }
1393
- } catch (err) {
1394
- const msg = err instanceof Error ? err.message : "An error occurred";
1395
- setError(msg);
1396
- variableStore.set("payment.error", msg);
1397
- if (showToasts) sonner.toast.error(msg);
1398
- onError?.(msg);
1399
- } finally {
1400
- variableStore.set("payment.loading", false);
1401
1519
  }
1402
- }, [stripe, elements, paymentMode, validateOnly, variableStore, campaignId, tracker, products, onSuccess, onError]);
1403
- react.useImperativeHandle(ref, () => ({ submit: handleSubmit }), [handleSubmit]);
1404
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1405
- /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.PaymentElement, { options: { layout: layout || "tabs" } }),
1406
- error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#ef4444", fontSize: "14px", marginTop: "12px" }, children: error })
1407
- ] });
1520
+ )
1521
+ });
1522
+ });
1523
+ function trigger(onClick, { asChild, children, className, disabled }) {
1524
+ if (asChild && react.isValidElement(children)) {
1525
+ const child = children;
1526
+ return react.cloneElement(child, { onClick });
1527
+ }
1528
+ return react.createElement("button", { type: "button", className, disabled, onClick }, children ?? "Continue");
1529
+ }
1530
+ function Checkout({
1531
+ product,
1532
+ surface,
1533
+ asChild,
1534
+ children,
1535
+ className,
1536
+ ...options
1537
+ }) {
1538
+ const driver = useDriver();
1539
+ const checkout = useCheckout(options);
1540
+ const intent = options.intent ?? "purchase";
1541
+ if (isInlineSurface(surface)) {
1542
+ return driver.renderInline ? react.createElement(
1543
+ "div",
1544
+ { key: product, className },
1545
+ driver.renderInline({ product, intent, surface }, checkout.callbacksFor(product, surface))
1546
+ ) : null;
1547
+ }
1548
+ if (surface === "sheet") {
1549
+ const cb = checkout.callbacksFor(product, surface);
1550
+ const open = () => {
1551
+ cb.onStart();
1552
+ showModal(CheckoutSheet, { product, surface, intent, onSuccess: cb.onSuccess, onError: cb.onError });
1553
+ };
1554
+ return trigger(open, { asChild, children, className, disabled: checkout.isLoading });
1408
1555
  }
1409
- );
1410
- var RealStripePaymentForm = react.forwardRef(
1411
- function RealStripePaymentForm2({
1412
- productId,
1413
- mode = "checkout",
1414
- variant = "elements",
1415
- successPageKey,
1416
- automaticTax = false,
1417
- managedPayments = false,
1418
- allowPromotionCodes = false,
1419
- onSuccess,
1420
- onError,
1421
- onReady,
1556
+ return trigger(() => checkout.open(product, surface), {
1557
+ asChild,
1558
+ children,
1422
1559
  className,
1423
- appearance,
1424
- layout
1425
- }, ref) {
1426
- const { campaignId, tracker, variableStore, products, router, config } = chunk7BSBNWJ6_cjs.useFunnelContext();
1427
- const showToasts = !config.settings?.disableToasts;
1428
- const [email] = useVariable("user.email");
1429
- const validateOnly = mode === "validate-only";
1430
- const product = react.useMemo(() => {
1431
- if (productId) return products.find((p) => p.id === productId) || null;
1432
- const selectedId = variableStore.get("products.selectedProductId");
1433
- return products.find((p) => p.id === selectedId) || null;
1434
- }, [productId, products, variableStore]);
1435
- const paymentMode = react.useMemo(() => {
1436
- if (validateOnly) return "payment";
1437
- if (product?.hasTrial && !product.paidTrial) return "setup";
1438
- return "payment";
1439
- }, [product, validateOnly]);
1440
- const [stripePromise, setStripePromise] = react.useState(null);
1441
- const [clientSecret, setClientSecret] = react.useState(null);
1442
- const [error, setError] = react.useState(null);
1443
- const [isLoading, setIsLoading] = react.useState(false);
1444
- const [alreadyCompleted, setAlreadyCompleted] = react.useState(false);
1445
- const hasInitialized = react.useRef(false);
1446
- react.useEffect(() => {
1447
- if (!email || !campaignId || hasInitialized.current) return;
1448
- if (!product?.storePriceId) return;
1449
- hasInitialized.current = true;
1450
- setIsLoading(true);
1451
- variableStore.set("payment.loading", true);
1452
- const createIntent = async () => {
1453
- try {
1454
- if (variant === "embedded") {
1455
- const origin = typeof window !== "undefined" ? window.location.origin : "";
1456
- let returnUrl = origin + "/";
1457
- if (successPageKey) {
1458
- const pageUrl = router.getPageUrl(successPageKey);
1459
- returnUrl = `${origin}${pageUrl}?checkout=success&session_id={CHECKOUT_SESSION_ID}`;
1460
- }
1461
- const apiCheckoutMode = managedPayments ? "embedded" : variant;
1462
- const body = {
1463
- campaignId,
1464
- sessionId: tracker.getSessionId(),
1465
- customerEmail: email,
1466
- priceId: product.storePriceId,
1467
- returnUrl,
1468
- checkoutMode: apiCheckoutMode,
1469
- automaticTax,
1470
- managedPayments,
1471
- allowPromotionCodes
1472
- };
1473
- if (product.hasTrial && product.trialDays > 0) {
1474
- body.trialPeriodDays = product.trialDays;
1475
- if (product.paidTrial && product.trialStorePriceId) {
1476
- body.paidTrialPriceId = product.trialStorePriceId;
1477
- }
1478
- }
1479
- const response = await fetch(
1480
- `${API_BASE_URL2}/campaign/${campaignId}/stripe/checkout-session`,
1481
- {
1482
- method: "POST",
1483
- headers: { "Content-Type": "application/json" },
1484
- body: JSON.stringify(body)
1485
- }
1486
- );
1487
- const result = await response.json();
1488
- if (!result.success) throw new Error(result.error || "Failed to create checkout session");
1489
- if (result.alreadyCompleted) {
1490
- variableStore.set("payment.error", "");
1491
- if (result.subscriptionId) {
1492
- variableStore.set("stripe.subscriptionId", result.subscriptionId);
1493
- variableStore.set("subscription.status", result.status);
1494
- }
1495
- if (result.paymentIntentId) {
1496
- variableStore.set("stripe.paymentIntentId", result.paymentIntentId);
1497
- variableStore.set("payment.status", "succeeded");
1498
- }
1499
- tracker.track("checkout.already_completed");
1500
- setAlreadyCompleted(true);
1501
- if (variant === "embedded" && successPageKey) {
1502
- router.goToPage(successPageKey);
1503
- } else {
1504
- onSuccess?.();
1505
- }
1506
- return;
1507
- }
1508
- setStripePromise(stripeJs.loadStripe(result.publishableKey));
1509
- setClientSecret(result.clientSecret);
1510
- } else {
1511
- const endpoint = paymentMode === "setup" ? `/campaign/${campaignId}/stripe/setup-intent` : `/campaign/${campaignId}/stripe/payment-intent`;
1512
- const body = {
1513
- campaignId,
1514
- sessionId: tracker.getSessionId(),
1515
- customerEmail: email,
1516
- priceId: product.storePriceId
1517
- };
1518
- if (validateOnly) body.validateOnly = true;
1519
- const response = await fetch(`${API_BASE_URL2}${endpoint}`, {
1520
- method: "POST",
1521
- headers: { "Content-Type": "application/json" },
1522
- body: JSON.stringify(body)
1523
- });
1524
- const result = await response.json();
1525
- if (!result.success) throw new Error(result.error || "Failed to create intent");
1526
- setStripePromise(stripeJs.loadStripe(result.publishableKey));
1527
- setClientSecret(result.clientSecret);
1528
- variableStore.set("user.stripeCustomerId", result.customerId);
1529
- }
1530
- tracker.track("checkout.start", {
1531
- productId: product.id,
1532
- priceId: product.stripePriceId || product.storePriceId,
1533
- productName: product.displayName
1534
- });
1535
- } catch (err) {
1536
- const msg = err instanceof Error ? err.message : "Failed to initialize payment";
1537
- setError(msg);
1538
- variableStore.set("payment.error", msg);
1539
- if (showToasts) sonner.toast.error(msg);
1540
- } finally {
1541
- setIsLoading(false);
1542
- variableStore.set("payment.loading", false);
1543
- }
1544
- };
1545
- createIntent();
1546
- }, [email, campaignId, product, paymentMode, validateOnly, variant, successPageKey, automaticTax, managedPayments, allowPromotionCodes, router, tracker, variableStore]);
1547
- if (alreadyCompleted) {
1548
- return null;
1549
- }
1550
- if (isLoading) {
1551
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Loading payment form..." });
1552
- }
1553
- if (!email) {
1554
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: "Email is required to initialize payment" });
1560
+ disabled: checkout.isLoading
1561
+ });
1562
+ }
1563
+ function Upsell({ product, fallback, asChild, children, className, ...options }) {
1564
+ const checkout = useCheckout({ ...options, intent: "upsell" });
1565
+ const start = async () => {
1566
+ const result = await checkout.open(product);
1567
+ if (result.ok || !fallback) return;
1568
+ const cb = checkout.callbacksFor(product, fallback);
1569
+ if (fallback === "sheet") {
1570
+ cb.onStart();
1571
+ showModal(CheckoutSheet, { product, surface: "sheet", intent: "upsell", onSuccess: cb.onSuccess, onError: cb.onError });
1572
+ } else {
1573
+ await checkout.open(product, fallback);
1555
1574
  }
1556
- if (error) {
1557
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: error });
1575
+ };
1576
+ return trigger(start, {
1577
+ asChild,
1578
+ children: children ?? "Add to my plan",
1579
+ className,
1580
+ disabled: checkout.isLoading
1581
+ });
1582
+ }
1583
+ function FunnelToaster(props = {}) {
1584
+ return react.createElement(sonner.Toaster, { position: "top-center", ...props });
1585
+ }
1586
+ function renderToaster(toaster) {
1587
+ if (toaster === false) return null;
1588
+ return react.createElement(sonner.Toaster, { position: "top-center", ...toaster === true ? {} : toaster });
1589
+ }
1590
+
1591
+ // src/state/namespaces.ts
1592
+ var Namespace = class {
1593
+ constructor(store, name, onWrite) {
1594
+ this.store = store;
1595
+ this.name = name;
1596
+ this.onWrite = onWrite;
1597
+ this.dot = `${NAMESPACE_PREFIX[name]}.`;
1598
+ }
1599
+ /** Fully-qualified store key for a namespace-relative key. */
1600
+ storeKey(key) {
1601
+ return this.dot + key;
1602
+ }
1603
+ /** The store-key prefix this namespace reads/writes (e.g. `user.`). */
1604
+ get prefix() {
1605
+ return this.dot;
1606
+ }
1607
+ /** Read the whole namespace as a flat object (prefix stripped). */
1608
+ getAll() {
1609
+ const out = {};
1610
+ const all = this.store.getState();
1611
+ for (const key in all) {
1612
+ if (key.startsWith(this.dot)) out[key.slice(this.dot.length)] = all[key];
1558
1613
  }
1559
- if (!stripePromise || !clientSecret) {
1560
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Initializing payment..." });
1614
+ return out;
1615
+ }
1616
+ get(key) {
1617
+ return key === void 0 ? this.getAll() : this.store.get(this.storeKey(key));
1618
+ }
1619
+ /** Write one or more keys from a plain object (batched + side-effected). */
1620
+ set(values) {
1621
+ const updates = {};
1622
+ for (const key in values) {
1623
+ updates[this.storeKey(key)] = values[key];
1561
1624
  }
1562
- if (variant === "embedded") {
1563
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(
1564
- reactStripeJs.EmbeddedCheckoutProvider,
1565
- {
1566
- stripe: stripePromise,
1567
- options: { clientSecret },
1568
- children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.EmbeddedCheckout, {})
1569
- }
1570
- ) });
1625
+ this.store.setMany(updates);
1626
+ this.onWrite(this.name, values);
1627
+ }
1628
+ };
1629
+ function createFunnel(store, context, tracker, experiments = []) {
1630
+ const onWrite = (ns, values) => {
1631
+ if (ns === "user" && typeof values.email === "string" && values.email) {
1632
+ tracker.identify(values.email);
1571
1633
  }
1572
- const defaultAppearance = {
1573
- theme: "stripe",
1574
- variables: { colorPrimary: "#3b82f6", borderRadius: "8px" }
1634
+ tracker.setVariables?.(store.getState());
1635
+ };
1636
+ const user = new Namespace(store, "user", onWrite);
1637
+ const responses = new Namespace(store, "responses", onWrite);
1638
+ const data = new Namespace(store, "data", onWrite);
1639
+ return {
1640
+ user,
1641
+ responses,
1642
+ data,
1643
+ context,
1644
+ experiments,
1645
+ track: (event, eventData, userData) => tracker.track(event, eventData, userData),
1646
+ snapshot: () => ({
1647
+ user: user.getAll(),
1648
+ responses: responses.getAll(),
1649
+ data: data.getAll(),
1650
+ context
1651
+ })
1652
+ };
1653
+ }
1654
+ var FunnelCtx = react.createContext(null);
1655
+ function FunnelProvider({
1656
+ config,
1657
+ sessionValues,
1658
+ tracker,
1659
+ context: contextOpts,
1660
+ products,
1661
+ experiments,
1662
+ checkout,
1663
+ messages,
1664
+ locale: localeOverride,
1665
+ toaster = true,
1666
+ store,
1667
+ children
1668
+ }) {
1669
+ const resolvedTracker = tracker ?? FALLBACK_TRACKER;
1670
+ const [value] = react.useState(() => {
1671
+ const s = store ?? createFunnelStore(
1672
+ { responses: config?.responses, data: config?.data },
1673
+ sessionValues
1674
+ );
1675
+ const ctx = buildContext({
1676
+ funnelId: config?.id,
1677
+ ...contextOpts
1678
+ });
1679
+ const bus = createBus({
1680
+ getVariable: (key) => s.get(key),
1681
+ getVariables: () => s.getState(),
1682
+ getCurrentPageId: () => ctx.page.key || null,
1683
+ getCustomerId: () => ctx.identity.customerId,
1684
+ getVisitorId: () => ctx.identity.visitorId
1685
+ });
1686
+ const busTracker = withBus(resolvedTracker, bus);
1687
+ return {
1688
+ funnel: createFunnel(s, ctx, busTracker, experiments ?? []),
1689
+ store: s,
1690
+ context: ctx,
1691
+ catalog: buildCatalog(products ?? []),
1692
+ driver: checkout ?? createMockDriver(),
1693
+ tracker: busTracker,
1694
+ bus
1575
1695
  };
1576
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.Elements, { stripe: stripePromise, options: { clientSecret, appearance: appearance || defaultAppearance }, children: /* @__PURE__ */ jsxRuntime.jsx(
1577
- InnerPaymentForm,
1578
- {
1579
- ref,
1580
- paymentMode,
1581
- validateOnly,
1582
- onSuccess,
1583
- onError,
1584
- onReady,
1585
- layout
1586
- }
1587
- ) }) });
1696
+ });
1697
+ react.useEffect(() => attachBus(value.bus), [value.bus]);
1698
+ react.useEffect(() => {
1699
+ value.tracker.setAcquisition?.(buildAcquisition(value.context));
1700
+ }, [value]);
1701
+ const tree = react.createElement(TrackerProvider, {
1702
+ tracker: value.tracker,
1703
+ children: react.createElement(CheckoutDriverProvider, {
1704
+ driver: value.driver,
1705
+ children: react.createElement(CatalogProvider, {
1706
+ catalog: value.catalog,
1707
+ children: react.createElement(FunnelCtx.Provider, {
1708
+ value,
1709
+ children: react.createElement(LocaleProvider, {
1710
+ config: config?.locales,
1711
+ catalog: messages,
1712
+ detected: value.context.locale.locale,
1713
+ override: localeOverride,
1714
+ children: react.createElement(ModalRuntime, { children })
1715
+ })
1716
+ })
1717
+ })
1718
+ })
1719
+ });
1720
+ return react.createElement(react.Fragment, null, tree, renderToaster(toaster));
1721
+ }
1722
+ var FALLBACK_TRACKER = createConsoleTracker({ silent: true });
1723
+ function useFunnelCtx() {
1724
+ const ctx = react.useContext(FunnelCtx);
1725
+ if (!ctx) {
1726
+ throw new Error("useFunnel/useResponse/\u2026 must be used inside <FunnelProvider>");
1588
1727
  }
1589
- );
1590
- var StripePaymentForm = react.forwardRef(
1591
- function StripePaymentForm2(props, ref) {
1592
- if (isDevMode()) {
1593
- return /* @__PURE__ */ jsxRuntime.jsx(DemoStripePaymentForm, { ref, ...props });
1594
- }
1595
- return /* @__PURE__ */ jsxRuntime.jsx(RealStripePaymentForm, { ref, ...props });
1728
+ return ctx;
1729
+ }
1730
+ function useFunnel() {
1731
+ return useFunnelCtx().funnel;
1732
+ }
1733
+ function useStoreKey(store, key) {
1734
+ const subscribe = react.useCallback(
1735
+ (cb) => store.subscribe(cb, { keys: [key] }),
1736
+ [store, key]
1737
+ );
1738
+ const getSnapshot = react.useCallback(() => store.get(key), [store, key]);
1739
+ return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1740
+ }
1741
+ function makeFieldHook(ns) {
1742
+ return function useNamespaceField(key) {
1743
+ const { funnel, store } = useFunnelCtx();
1744
+ const value = useStoreKey(store, `${NAMESPACE_PREFIX[ns]}.${key}`);
1745
+ const set = react.useCallback(
1746
+ (v) => funnel[ns].set({ [key]: v }),
1747
+ [funnel, key]
1748
+ );
1749
+ return [value, set];
1750
+ };
1751
+ }
1752
+ var useResponse = makeFieldHook("responses");
1753
+ var useUserAttribute = makeFieldHook("user");
1754
+ var useData = makeFieldHook("data");
1755
+ function useField(path) {
1756
+ const dot = path.indexOf(".");
1757
+ const ns = path.slice(0, dot);
1758
+ const key = path.slice(dot + 1);
1759
+ const { funnel, store } = useFunnelCtx();
1760
+ if (!(ns in NAMESPACE_PREFIX)) {
1761
+ throw new Error(`useField: "${path}" is not a writable namespace (user/responses/data)`);
1596
1762
  }
1597
- );
1598
- function PaddleCheckout({
1599
- productId,
1600
- mode = "overlay",
1601
- onSuccess,
1602
- onError,
1603
- className
1763
+ const value = useStoreKey(store, `${NAMESPACE_PREFIX[ns]}.${key}`);
1764
+ const set = react.useCallback(
1765
+ (v) => funnel[ns].set({ [key]: v }),
1766
+ [funnel, ns, key]
1767
+ );
1768
+ return { value, set };
1769
+ }
1770
+ function useContextObject() {
1771
+ return useFunnelCtx().context;
1772
+ }
1773
+ function useDevice() {
1774
+ return useContextObject().device;
1775
+ }
1776
+ function useLocale() {
1777
+ return useContextObject().locale;
1778
+ }
1779
+ function useUtm() {
1780
+ return useContextObject().utm;
1781
+ }
1782
+ function useClickIds() {
1783
+ return useContextObject().clickIds;
1784
+ }
1785
+ function useSystem() {
1786
+ return useContextObject().system;
1787
+ }
1788
+ function useContextValue(path) {
1789
+ const ctx = useContextObject();
1790
+ return react.useMemo(
1791
+ () => path.split(".").reduce((acc, k) => acc?.[k], ctx),
1792
+ [ctx, path]
1793
+ );
1794
+ }
1795
+ function Choice({
1796
+ bind,
1797
+ options,
1798
+ className,
1799
+ selectedClassName,
1800
+ advanceOnSelect
1801
+ }) {
1802
+ const { value, set } = useField(bind);
1803
+ const nav = useNavigation();
1804
+ return options.map((opt) => {
1805
+ const o = typeof opt === "string" ? { value: opt } : opt;
1806
+ const selected = value === o.value;
1807
+ return react.createElement(
1808
+ "button",
1809
+ {
1810
+ key: o.value,
1811
+ type: "button",
1812
+ "aria-pressed": selected,
1813
+ "data-selected": selected || void 0,
1814
+ className: selected && selectedClassName ? `${className ?? ""} ${selectedClassName}`.trim() : className,
1815
+ onClick: () => {
1816
+ set(o.value);
1817
+ if (advanceOnSelect) nav.next();
1818
+ }
1819
+ },
1820
+ o.label ?? o.value
1821
+ );
1822
+ });
1823
+ }
1824
+ function Next({ children, onClick, ...rest }) {
1825
+ const { next } = useNavigation();
1826
+ return react.createElement(
1827
+ "button",
1828
+ {
1829
+ type: "button",
1830
+ ...rest,
1831
+ onClick: (e) => {
1832
+ onClick?.(e);
1833
+ if (!e.defaultPrevented) next();
1834
+ }
1835
+ },
1836
+ children ?? "Continue"
1837
+ );
1838
+ }
1839
+ function Back({ children, onClick, ...rest }) {
1840
+ const { back, canGoBack } = useNavigation();
1841
+ if (!canGoBack) return null;
1842
+ return react.createElement(
1843
+ "button",
1844
+ {
1845
+ type: "button",
1846
+ ...rest,
1847
+ onClick: (e) => {
1848
+ onClick?.(e);
1849
+ if (!e.defaultPrevented) back();
1850
+ }
1851
+ },
1852
+ children ?? "Back"
1853
+ );
1854
+ }
1855
+
1856
+ // src/tracking/catalog.ts
1857
+ var EVENT_CATALOG = {
1858
+ // ── Runtime / navigation (JS subscriptions; not persisted) ──
1859
+ "funnel.start": { sources: ["frontend"], tracking: false, integrations: false, runtime: true },
1860
+ "experiment.exposure": { sources: ["frontend"], tracking: true, integrations: false },
1861
+ // ── Frontend, tracked; some also go to pixels ──
1862
+ "page.view": { sources: ["frontend"], tracking: true, integrations: true },
1863
+ "page.exit": { sources: ["frontend"], tracking: true, integrations: false },
1864
+ "user.registered": { sources: ["frontend"], tracking: true, integrations: true },
1865
+ "checkout.start": { sources: ["frontend"], tracking: true, integrations: true },
1866
+ "checkout.payment_added": { sources: ["frontend"], tracking: true, integrations: true },
1867
+ "checkout.failed": { sources: ["frontend"], tracking: true, integrations: false },
1868
+ // ── Backend-originated money events (server mints the eventId; client fires the pixel) ──
1869
+ "purchase.complete": { sources: ["backend"], tracking: true, integrations: true },
1870
+ "customer.first_purchase": { sources: ["backend"], tracking: true, integrations: true },
1871
+ "subscription.created": { sources: ["backend"], tracking: true, integrations: true },
1872
+ "subscription.renewal": { sources: ["backend"], tracking: true, integrations: true }
1873
+ };
1874
+ function isTrackableEvent(name) {
1875
+ return EVENT_CATALOG[name]?.tracking ?? false;
1876
+ }
1877
+ function isIntegrationEvent(name) {
1878
+ return EVENT_CATALOG[name]?.integrations ?? false;
1879
+ }
1880
+ function integrationEvents() {
1881
+ return Object.keys(EVENT_CATALOG).filter((e) => EVENT_CATALOG[e].integrations);
1882
+ }
1883
+ var injected = /* @__PURE__ */ new Set();
1884
+ function Script({
1885
+ src,
1886
+ strategy = "afterInteractive",
1887
+ id,
1888
+ async = true,
1889
+ defer,
1890
+ nonce,
1891
+ attributes,
1892
+ onLoad,
1893
+ onError
1604
1894
  }) {
1605
- const { variableStore, tracker, products } = chunk7BSBNWJ6_cjs.useFunnelContext();
1606
- const containerRef = react.useRef(null);
1607
- const initializedRef = react.useRef(false);
1608
- const product = productId ? products.find((p) => p.id === productId) : products.find((p) => p.id === variableStore.get("products.selectedProductId"));
1609
- const handleCheckoutComplete = react.useCallback(() => {
1610
- tracker.track("purchase.complete", {
1611
- amount: product?.rawPrice ? product.rawPrice / 100 : 0,
1612
- currency: product?.currencyCode || "USD"
1613
- });
1614
- onSuccess?.();
1615
- }, [tracker, product, onSuccess]);
1616
1895
  react.useEffect(() => {
1617
- if (initializedRef.current || !product?.paddlePriceId) return;
1618
- initializedRef.current = true;
1619
- if (!window.Paddle) {
1620
- onError?.("Paddle.js not loaded. Include the Paddle script in your HTML.");
1896
+ if (typeof document === "undefined") return;
1897
+ const key = id ?? src;
1898
+ if (injected.has(key)) {
1899
+ onLoad?.();
1621
1900
  return;
1622
1901
  }
1623
- const email = variableStore.get("user.email") || "";
1624
- if (mode === "overlay") {
1625
- window.Paddle.Checkout.open({
1626
- items: [{ priceId: product.paddlePriceId, quantity: 1 }],
1627
- customer: email ? { email } : void 0,
1628
- successCallback: handleCheckoutComplete,
1629
- closeCallback: () => {
1630
- }
1902
+ const inject = () => {
1903
+ if (injected.has(key)) return;
1904
+ injected.add(key);
1905
+ const el = document.createElement("script");
1906
+ el.src = src;
1907
+ if (id) el.id = id;
1908
+ if (async) el.async = true;
1909
+ if (defer) el.defer = true;
1910
+ if (nonce) el.nonce = nonce;
1911
+ if (attributes) for (const [k, v] of Object.entries(attributes)) el.setAttribute(k, v);
1912
+ el.addEventListener("load", () => onLoad?.());
1913
+ el.addEventListener("error", () => {
1914
+ injected.delete(key);
1915
+ onError?.();
1631
1916
  });
1632
- } else {
1633
- if (containerRef.current) {
1634
- window.Paddle.Checkout.open({
1635
- items: [{ priceId: product.paddlePriceId, quantity: 1 }],
1636
- customer: email ? { email } : void 0,
1637
- settings: {
1638
- displayMode: "inline",
1639
- frameTarget: containerRef.current.id || "paddle-checkout",
1640
- frameInitialHeight: 450,
1641
- frameStyle: "width: 100%; min-width: 312px; background-color: transparent; border: none;"
1642
- },
1643
- successCallback: handleCheckoutComplete
1644
- });
1917
+ document.head.appendChild(el);
1918
+ };
1919
+ if (strategy === "lazyOnload") {
1920
+ const ric = window.requestIdleCallback;
1921
+ if (ric) {
1922
+ ric(inject);
1923
+ } else {
1924
+ const t = setTimeout(inject, 0);
1925
+ return () => clearTimeout(t);
1645
1926
  }
1927
+ return;
1646
1928
  }
1647
- tracker.track("checkout.start", {
1648
- productId: product.id,
1649
- priceId: product.paddlePriceId,
1650
- productName: product.displayName
1651
- });
1652
- }, [product, mode, variableStore, tracker, handleCheckoutComplete, onError]);
1653
- if (!product) {
1654
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: "No product selected" });
1655
- }
1656
- if (mode === "inline") {
1657
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, id: "paddle-checkout", className });
1658
- }
1929
+ inject();
1930
+ }, [src, id]);
1659
1931
  return null;
1660
1932
  }
1661
1933
 
1662
- Object.defineProperty(exports, "useNavigation", {
1663
- enumerable: true,
1664
- get: function () { return chunk7D7VJBOV_cjs.useNavigation; }
1665
- });
1666
- Object.defineProperty(exports, "useResponse", {
1667
- enumerable: true,
1668
- get: function () { return chunk7D7VJBOV_cjs.useResponse; }
1669
- });
1670
- Object.defineProperty(exports, "useResponses", {
1671
- enumerable: true,
1672
- get: function () { return chunk7D7VJBOV_cjs.useResponses; }
1673
- });
1674
- Object.defineProperty(exports, "FunnelProvider", {
1675
- enumerable: true,
1676
- get: function () { return chunk7BSBNWJ6_cjs.FunnelProvider; }
1677
- });
1678
- Object.defineProperty(exports, "registerIntegration", {
1679
- enumerable: true,
1680
- get: function () { return chunk7BSBNWJ6_cjs.registerIntegration; }
1681
- });
1934
+ // src/manifest/manifest.ts
1935
+ function isVariantNode(page) {
1936
+ return isVariantKey(page.key);
1937
+ }
1938
+ function serializeGate(when) {
1939
+ if (!when) return void 0;
1940
+ if (typeof when === "function") return { kind: "predicate" };
1941
+ return { kind: "declarative", condition: when };
1942
+ }
1943
+ function compileManifest(input) {
1944
+ const { funnel, pages } = input;
1945
+ const keys = new Set(pages.map((p) => p.key));
1946
+ const manifestPages = pages.map((p, index) => ({
1947
+ id: p.key,
1948
+ key: p.key,
1949
+ type: p.meta?.type ?? "default",
1950
+ slug: p.meta?.slug ?? p.key,
1951
+ index,
1952
+ variantOf: isVariantKey(p.key) ? parseSlotKey(p.key).slot : void 0
1953
+ }));
1954
+ const edges = [];
1955
+ const badTargets = [];
1956
+ const nextAnchor = (from) => {
1957
+ for (let j = from + 1; j < pages.length; j++) {
1958
+ if (!isVariantNode(pages[j])) return pages[j];
1959
+ }
1960
+ return void 0;
1961
+ };
1962
+ pages.forEach((p, i) => {
1963
+ if (isVariantNode(p)) return;
1964
+ const routes = p.meta?.next ?? [];
1965
+ let hasUnconditional = false;
1966
+ for (const route of routes) {
1967
+ if (!keys.has(route.to)) {
1968
+ badTargets.push({ from: p.key, to: route.to });
1969
+ continue;
1970
+ }
1971
+ edges.push({ from: p.key, to: route.to, kind: "branch", condition: serializeGate(route.when) });
1972
+ if (!route.when) hasUnconditional = true;
1973
+ }
1974
+ const next = nextAnchor(i);
1975
+ if (!hasUnconditional && next && p.meta?.type !== "finish") {
1976
+ edges.push({ from: p.key, to: next.key, kind: "linear" });
1977
+ }
1978
+ });
1979
+ const startPage = pages.find((p) => !isVariantNode(p));
1980
+ const start = startPage?.key ?? null;
1981
+ const variantKeys = new Set(pages.filter(isVariantNode).map((p) => p.key));
1982
+ const validation = validate(manifestPages, edges, badTargets, start, variantKeys);
1983
+ return {
1984
+ id: funnel.id,
1985
+ pages: manifestPages,
1986
+ flow: { start, nodes: manifestPages.map((p) => p.id), edges },
1987
+ products: funnel.products ?? [],
1988
+ locales: funnel.locales,
1989
+ validation
1990
+ };
1991
+ }
1992
+ function validate(pages, edges, badTargets, start, variantKeys) {
1993
+ const outgoing = /* @__PURE__ */ new Map();
1994
+ for (const e of edges) outgoing.set(e.from, (outgoing.get(e.from) ?? 0) + 1);
1995
+ const deadEnds = pages.filter((p) => p.type !== "finish" && !variantKeys.has(p.id) && !(outgoing.get(p.id) > 0)).map((p) => p.id);
1996
+ const reachable = /* @__PURE__ */ new Set();
1997
+ if (start) {
1998
+ const adj = /* @__PURE__ */ new Map();
1999
+ for (const e of edges) {
2000
+ const list = adj.get(e.from);
2001
+ if (list) list.push(e.to);
2002
+ else adj.set(e.from, [e.to]);
2003
+ }
2004
+ const stack = [start];
2005
+ while (stack.length) {
2006
+ const id = stack.pop();
2007
+ if (reachable.has(id)) continue;
2008
+ reachable.add(id);
2009
+ for (const to of adj.get(id) ?? []) stack.push(to);
2010
+ }
2011
+ }
2012
+ const unreachable = pages.filter((p) => !variantKeys.has(p.id) && !reachable.has(p.id)).map((p) => p.id);
2013
+ const missingEmailCapture = !pages.some((p) => p.type === "email-capture");
2014
+ return {
2015
+ ok: deadEnds.length === 0 && unreachable.length === 0 && badTargets.length === 0 && !missingEmailCapture,
2016
+ deadEnds,
2017
+ unreachable,
2018
+ badTargets,
2019
+ missingEmailCapture
2020
+ };
2021
+ }
2022
+
1682
2023
  Object.defineProperty(exports, "toast", {
1683
2024
  enumerable: true,
1684
2025
  get: function () { return sonner.toast; }
1685
2026
  });
1686
- exports.PaddleCheckout = PaddleCheckout;
1687
- exports.StripePaymentForm = StripePaymentForm;
1688
- exports.defineConfig = defineConfig;
2027
+ exports.Back = Back;
2028
+ exports.Checkout = Checkout;
2029
+ exports.Choice = Choice;
2030
+ exports.EVENT_CATALOG = EVENT_CATALOG;
2031
+ exports.FunnelProvider = FunnelProvider;
2032
+ exports.FunnelToaster = FunnelToaster;
2033
+ exports.FunnelView = FunnelView;
2034
+ exports.INLINE_SURFACES = INLINE_SURFACES;
2035
+ exports.Modal = Modal;
2036
+ exports.Next = Next;
2037
+ exports.PROVIDER_PROFILES = PROVIDER_PROFILES;
2038
+ exports.Script = Script;
2039
+ exports.Sheet = Sheet;
2040
+ exports.Upsell = Upsell;
2041
+ exports.VariableStore = VariableStore;
2042
+ exports.assignVariant = assignVariant;
2043
+ exports.attachBus = attachBus;
2044
+ exports.bucketingSeed = bucketingSeed;
2045
+ exports.buildAcquisition = buildAcquisition;
2046
+ exports.buildCatalog = buildCatalog;
2047
+ exports.checkoutError = checkoutError;
2048
+ exports.compileManifest = compileManifest;
2049
+ exports.createBus = createBus;
2050
+ exports.createConsoleTracker = createConsoleTracker;
2051
+ exports.createFunnelStore = createFunnelStore;
2052
+ exports.createMockDriver = createMockDriver;
2053
+ exports.defineFunnel = defineFunnel;
2054
+ exports.defineModal = defineModal;
1689
2055
  exports.definePage = definePage;
2056
+ exports.entryGuard = entryGuard;
2057
+ exports.evaluateCondition = evaluateCondition;
2058
+ exports.evaluateGate = evaluateGate;
2059
+ exports.expectedPathLength = expectedPathLength;
2060
+ exports.fnv1a = fnv1a;
2061
+ exports.formatProduct = formatProduct;
2062
+ exports.hashToUnit = hashToUnit;
2063
+ exports.hideModal = hideModal;
2064
+ exports.integrationEvents = integrationEvents;
2065
+ exports.isInlineSurface = isInlineSurface;
2066
+ exports.isIntegrationEvent = isIntegrationEvent;
2067
+ exports.isMerchantOfRecord = isMerchantOfRecord;
2068
+ exports.isOrchestrator = isOrchestrator;
2069
+ exports.isRtl = isRtl;
2070
+ exports.isTrackableEvent = isTrackableEvent;
2071
+ exports.isVariantKey = isVariantKey;
2072
+ exports.newEventId = newEventId;
2073
+ exports.nextPage = nextPage;
2074
+ exports.outgoingKeys = outgoingKeys;
2075
+ exports.pageMeta = pageMeta;
2076
+ exports.parseSlotKey = parseSlotKey;
2077
+ exports.pickByWeight = pickByWeight;
2078
+ exports.registerModal = registerModal;
2079
+ exports.removeModal = removeModal;
2080
+ exports.resolveExperiments = resolveExperiments;
2081
+ exports.resolveLocale = resolveLocale;
2082
+ exports.resolveProduct = resolveProduct;
2083
+ exports.resolveRoute = resolveRoute;
2084
+ exports.showModal = showModal;
2085
+ exports.surfacesFor = surfacesFor;
2086
+ exports.unregisterModal = unregisterModal;
2087
+ exports.useCheckout = useCheckout;
2088
+ exports.useClickIds = useClickIds;
2089
+ exports.useContextValue = useContextValue;
1690
2090
  exports.useData = useData;
1691
- exports.useDateOfBirth = useDateOfBirth;
1692
- exports.useDeviceInfo = useDeviceInfo;
2091
+ exports.useDevice = useDevice;
2092
+ exports.useExperiment = useExperiment;
2093
+ exports.useField = useField;
1693
2094
  exports.useFunnel = useFunnel;
1694
- exports.useKeyboard = useKeyboard;
1695
2095
  exports.useLocale = useLocale;
1696
- exports.usePageData = usePageData;
1697
- exports.usePayment = usePayment;
2096
+ exports.useModal = useModal;
2097
+ exports.useNavigation = useNavigation;
2098
+ exports.usePage = usePage;
2099
+ exports.useProduct = useProduct;
1698
2100
  exports.useProducts = useProducts;
1699
- exports.useQueryParam = useQueryParam;
1700
- exports.useQueryParams = useQueryParams;
1701
- exports.useSafeArea = useSafeArea;
1702
- exports.useTracking = useTracking;
2101
+ exports.useResponse = useResponse;
2102
+ exports.useSystem = useSystem;
2103
+ exports.useTracker = useTracker;
1703
2104
  exports.useTranslation = useTranslation;
1704
- exports.useUser = useUser;
1705
- exports.useUserProperty = useUserProperty;
1706
- exports.useVariable = useVariable;
1707
- exports.useVariables = useVariables;
2105
+ exports.useUserAttribute = useUserAttribute;
2106
+ exports.useUtm = useUtm;
2107
+ exports.validateCheckout = validateCheckout;
2108
+ exports.validateExperiments = validateExperiments;
2109
+ exports.validateUpsell = validateUpsell;
2110
+ exports.withBus = withBus;
1708
2111
  //# sourceMappingURL=index.cjs.map
1709
2112
  //# sourceMappingURL=index.cjs.map