@appfunnel-dev/sdk 0.5.0 → 0.7.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.
@@ -0,0 +1,1321 @@
1
+ import { createContext, useContext, useRef, useMemo, useCallback, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+
11
+ // src/runtime/integrations.ts
12
+ var AppFunnelEventBus = class {
13
+ constructor(store, router, selectProduct) {
14
+ this.subscriptions = /* @__PURE__ */ new Map();
15
+ this.store = store;
16
+ this.router = router;
17
+ this.selectProduct = selectProduct;
18
+ }
19
+ /** Attach the event bus to window.appfunnel */
20
+ attach() {
21
+ if (typeof window === "undefined") return;
22
+ const self = this;
23
+ window.appfunnel = {
24
+ // Event subscriptions
25
+ on(eventType, callback) {
26
+ return self.on(eventType, callback);
27
+ },
28
+ off(eventType, callback) {
29
+ self.off(eventType, callback);
30
+ },
31
+ // Internal — called by FunnelTracker.emitToRuntime()
32
+ _emitEvent(eventType, data) {
33
+ self.emit(eventType, data);
34
+ },
35
+ // Getters for integration loaders that need them
36
+ getVariable(variableId) {
37
+ return self.store.get(variableId);
38
+ },
39
+ getVariables() {
40
+ return { ...self.store.getState() };
41
+ },
42
+ getCurrentPageId() {
43
+ return self.router.getCurrentPage()?.key ?? null;
44
+ },
45
+ getCustomerId() {
46
+ return self.store.get("user.stripeCustomerId") || null;
47
+ },
48
+ isPaymentAuthorized() {
49
+ return !!self.store.get("user.stripeCustomerId");
50
+ },
51
+ // Methods
52
+ setVariable(variableId, value) {
53
+ self.store.set(variableId, value);
54
+ },
55
+ selectProduct(productId) {
56
+ self.selectProduct(productId);
57
+ },
58
+ goToNextPage() {
59
+ self.router.goToNextPage(self.store.getState());
60
+ },
61
+ goBack() {
62
+ self.router.goBack();
63
+ },
64
+ openUrl(url) {
65
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
66
+ },
67
+ callEvent(eventName, data) {
68
+ self.emit(eventName, data);
69
+ },
70
+ // Debug
71
+ debug: false,
72
+ setDebug(_enabled) {
73
+ }
74
+ };
75
+ }
76
+ /** Emit an event to all subscribers */
77
+ emit(eventType, data) {
78
+ const subs = this.subscriptions.get(eventType);
79
+ if (!subs || subs.length === 0) return;
80
+ for (const sub of subs) {
81
+ try {
82
+ sub.callback(data);
83
+ } catch (error) {
84
+ console.error(`[AppFunnel] Error in event handler for "${eventType}":`, error);
85
+ }
86
+ }
87
+ }
88
+ /** Clean up page-scoped subscriptions (called on page change) */
89
+ onPageChange() {
90
+ for (const [eventType, subs] of this.subscriptions.entries()) {
91
+ this.subscriptions.set(eventType, subs.filter((s) => s.scope === "global"));
92
+ }
93
+ }
94
+ /** Destroy and detach from window */
95
+ destroy() {
96
+ this.subscriptions.clear();
97
+ if (typeof window !== "undefined") {
98
+ delete window.appfunnel;
99
+ }
100
+ }
101
+ on(eventType, callback) {
102
+ if (!this.subscriptions.has(eventType)) {
103
+ this.subscriptions.set(eventType, []);
104
+ }
105
+ const sub = { callback, scope: "global" };
106
+ this.subscriptions.get(eventType).push(sub);
107
+ return () => this.off(eventType, callback);
108
+ }
109
+ off(eventType, callback) {
110
+ const subs = this.subscriptions.get(eventType);
111
+ if (!subs) return;
112
+ const index = subs.findIndex((s) => s.callback === callback);
113
+ if (index > -1) subs.splice(index, 1);
114
+ }
115
+ };
116
+ var loaders = {};
117
+ function registerIntegration(id, loader) {
118
+ loaders[id] = loader;
119
+ }
120
+ function initializeIntegrations(integrations) {
121
+ if (typeof window === "undefined") return;
122
+ window.__INTEGRATIONS__ = integrations;
123
+ for (const [integrationId, config] of Object.entries(integrations)) {
124
+ const loader = loaders[integrationId];
125
+ if (loader) {
126
+ try {
127
+ loader(config);
128
+ } catch (err) {
129
+ console.error(`[AppFunnel] Error loading integration ${integrationId}:`, err);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // src/runtime/variableStore.ts
136
+ var VariableStore = class {
137
+ constructor(initial) {
138
+ this.listeners = /* @__PURE__ */ new Set();
139
+ /** Tracks which keys changed in the last mutation — used by scoped notify */
140
+ this.changedKeys = [];
141
+ this.state = { ...initial };
142
+ }
143
+ getState() {
144
+ return this.state;
145
+ }
146
+ get(key) {
147
+ return this.state[key];
148
+ }
149
+ set(key, value) {
150
+ if (this.state[key] === value) return;
151
+ this.state = { ...this.state, [key]: value };
152
+ this.changedKeys = [key];
153
+ this.notify();
154
+ }
155
+ setState(updater) {
156
+ const prev = this.state;
157
+ const next = updater(prev);
158
+ if (next === prev) return;
159
+ this.state = next;
160
+ this.changedKeys = [];
161
+ for (const key of Object.keys(next)) {
162
+ if (next[key] !== prev[key]) this.changedKeys.push(key);
163
+ }
164
+ for (const key of Object.keys(prev)) {
165
+ if (!(key in next) && prev[key] !== void 0) this.changedKeys.push(key);
166
+ }
167
+ this.notify();
168
+ }
169
+ /** Batch set multiple variables at once */
170
+ setMany(updates) {
171
+ const changed = [];
172
+ const next = { ...this.state };
173
+ for (const [key, value] of Object.entries(updates)) {
174
+ if (next[key] !== value) {
175
+ next[key] = value;
176
+ changed.push(key);
177
+ }
178
+ }
179
+ if (changed.length === 0) return;
180
+ this.state = next;
181
+ this.changedKeys = changed;
182
+ this.notify();
183
+ }
184
+ /**
185
+ * Subscribe to store changes.
186
+ *
187
+ * @param listener - callback fired when relevant keys change
188
+ * @param scope - optional scope to limit notifications:
189
+ * - `{ keys: ['data.x', 'user.email'] }` — only fire when these exact keys change
190
+ * - `{ prefix: 'answers.' }` — only fire when any key starting with this prefix changes
191
+ * - omit or `{}` — fire on every change (global listener)
192
+ */
193
+ subscribe(listener, scope) {
194
+ const entry = {
195
+ listener,
196
+ keys: scope?.keys || null,
197
+ prefix: scope?.prefix || null
198
+ };
199
+ this.listeners.add(entry);
200
+ return () => this.listeners.delete(entry);
201
+ }
202
+ notify() {
203
+ const changed = this.changedKeys;
204
+ for (const entry of this.listeners) {
205
+ if (!entry.keys && !entry.prefix) {
206
+ entry.listener();
207
+ continue;
208
+ }
209
+ if (entry.keys) {
210
+ if (changed.some((k) => entry.keys.includes(k))) {
211
+ entry.listener();
212
+ }
213
+ continue;
214
+ }
215
+ if (entry.prefix) {
216
+ if (changed.some((k) => k.startsWith(entry.prefix))) {
217
+ entry.listener();
218
+ }
219
+ }
220
+ }
221
+ }
222
+ };
223
+ var BUILTIN_USER_VARIABLES = {
224
+ "user.email": "",
225
+ "user.name": "",
226
+ "user.stripeCustomerId": "",
227
+ "user.paddleCustomerId": ""
228
+ };
229
+ var BUILTIN_QUERY_VARIABLES = {
230
+ "query.utm_source": "",
231
+ "query.utm_medium": "",
232
+ "query.utm_campaign": "",
233
+ "query.utm_content": "",
234
+ "query.utm_term": ""
235
+ };
236
+ function collectQueryVariables() {
237
+ if (typeof window === "undefined") return {};
238
+ const params = new URLSearchParams(window.location.search);
239
+ const result = {};
240
+ params.forEach((value, key) => {
241
+ result[`query.${key}`] = value;
242
+ });
243
+ return result;
244
+ }
245
+ function createVariableStore(config, sessionValues) {
246
+ const initial = {};
247
+ Object.assign(initial, BUILTIN_USER_VARIABLES);
248
+ Object.assign(initial, BUILTIN_QUERY_VARIABLES);
249
+ Object.assign(initial, collectQueryVariables());
250
+ if (config.responses) {
251
+ for (const [key, cfg] of Object.entries(config.responses)) {
252
+ initial[`answers.${key}`] = cfg.default ?? getDefaultForType(cfg.type);
253
+ }
254
+ }
255
+ if (config.queryParams) {
256
+ for (const [key, cfg] of Object.entries(config.queryParams)) {
257
+ if (initial[`query.${key}`] === void 0) {
258
+ initial[`query.${key}`] = cfg.default ?? getDefaultForType(cfg.type);
259
+ }
260
+ }
261
+ }
262
+ if (config.data) {
263
+ for (const [key, cfg] of Object.entries(config.data)) {
264
+ initial[`data.${key}`] = cfg.default ?? getDefaultForType(cfg.type);
265
+ }
266
+ }
267
+ if (sessionValues) {
268
+ for (const [key, value] of Object.entries(sessionValues)) {
269
+ if (value !== void 0) {
270
+ initial[key] = value;
271
+ }
272
+ }
273
+ }
274
+ return new VariableStore(initial);
275
+ }
276
+ function getDefaultForType(type) {
277
+ switch (type) {
278
+ case "string":
279
+ return "";
280
+ case "number":
281
+ return 0;
282
+ case "boolean":
283
+ return false;
284
+ case "stringArray":
285
+ return [];
286
+ default:
287
+ return "";
288
+ }
289
+ }
290
+
291
+ // src/runtime/conditions.ts
292
+ function isConditionGroup(condition) {
293
+ return "operator" in condition && "rules" in condition;
294
+ }
295
+ function evaluateCondition(condition, variables) {
296
+ if (isConditionGroup(condition)) {
297
+ return evaluateConditionGroup(condition, variables);
298
+ }
299
+ return evaluateSimpleCondition(condition, variables);
300
+ }
301
+ function evaluateConditionGroup(group, variables) {
302
+ const { operator, rules } = group;
303
+ if (!rules || rules.length === 0) return true;
304
+ const results = rules.map((rule) => evaluateCondition(rule, variables));
305
+ if (operator === "AND") {
306
+ return results.every(Boolean);
307
+ }
308
+ return results.some(Boolean);
309
+ }
310
+ function evaluateSimpleCondition(condition, variables) {
311
+ const variableValue = variables[condition.variable];
312
+ if (condition.equals !== void 0) {
313
+ if (Array.isArray(variableValue)) {
314
+ return variableValue.length === Number(condition.equals);
315
+ }
316
+ return variableValue == condition.equals;
317
+ }
318
+ if (condition.notEquals !== void 0) {
319
+ if (Array.isArray(variableValue)) {
320
+ return variableValue.length !== Number(condition.notEquals);
321
+ }
322
+ return variableValue != condition.notEquals;
323
+ }
324
+ if (condition.contains !== void 0) {
325
+ if (typeof variableValue !== "string") return false;
326
+ return variableValue.includes(condition.contains);
327
+ }
328
+ if (condition.greaterThan !== void 0) {
329
+ if (Array.isArray(variableValue)) {
330
+ return variableValue.length > condition.greaterThan;
331
+ }
332
+ return Number(variableValue) > condition.greaterThan;
333
+ }
334
+ if (condition.lessThan !== void 0) {
335
+ if (Array.isArray(variableValue)) {
336
+ return variableValue.length < condition.lessThan;
337
+ }
338
+ return Number(variableValue) < condition.lessThan;
339
+ }
340
+ if (condition.exists !== void 0) {
341
+ const exists = variableValue !== void 0 && variableValue !== null && variableValue !== "";
342
+ return condition.exists ? exists : !exists;
343
+ }
344
+ if (condition.isEmpty !== void 0) {
345
+ let empty;
346
+ if (Array.isArray(variableValue)) {
347
+ empty = variableValue.length === 0;
348
+ } else if (typeof variableValue === "string") {
349
+ empty = variableValue.trim() === "";
350
+ } else {
351
+ empty = !variableValue;
352
+ }
353
+ return condition.isEmpty ? empty : !empty;
354
+ }
355
+ if (condition.includes !== void 0) {
356
+ if (!Array.isArray(variableValue)) return false;
357
+ return variableValue.includes(condition.includes);
358
+ }
359
+ return true;
360
+ }
361
+
362
+ // src/runtime/router.ts
363
+ var Router = class {
364
+ constructor(options) {
365
+ this.history = [];
366
+ this.listeners = /* @__PURE__ */ new Set();
367
+ const { config, initialPage, basePath, campaignSlug } = options;
368
+ this.config = config;
369
+ this.pageKeys = Object.keys(config.pages ?? {});
370
+ this.basePath = basePath || "";
371
+ const defaultInitial = config.initialPageKey || this.pageKeys[0] || "";
372
+ const isDev = typeof globalThis !== "undefined" && globalThis.__APPFUNNEL_DEV__;
373
+ if (initialPage && initialPage !== defaultInitial) {
374
+ if (isDev || !campaignSlug) {
375
+ this.initialKey = config.pages?.[initialPage] ? initialPage : defaultInitial;
376
+ } else {
377
+ const hasSession = this.hasSessionCookie(campaignSlug);
378
+ this.initialKey = hasSession && config.pages?.[initialPage] ? initialPage : defaultInitial;
379
+ }
380
+ } else {
381
+ this.initialKey = initialPage || defaultInitial;
382
+ }
383
+ this.currentKey = this.initialKey;
384
+ }
385
+ // ── Session check ──────────────────────────────────
386
+ hasSessionCookie(campaignSlug) {
387
+ if (!campaignSlug || typeof document === "undefined") return false;
388
+ const name = `fs_${campaignSlug}=`;
389
+ return document.cookie.split(";").some((c) => c.trim().startsWith(name));
390
+ }
391
+ // ── Subscriptions ────────────────────────────────────
392
+ subscribe(listener) {
393
+ this.listeners.add(listener);
394
+ return () => this.listeners.delete(listener);
395
+ }
396
+ notify() {
397
+ for (const listener of this.listeners) listener();
398
+ }
399
+ /** Snapshot key for useSyncExternalStore — changes on every navigation. */
400
+ getSnapshot() {
401
+ return this.currentKey;
402
+ }
403
+ // ── Reads ────────────────────────────────────────────
404
+ getCurrentPage() {
405
+ if (!this.currentKey) return null;
406
+ const pageConfig = this.config.pages?.[this.currentKey];
407
+ if (!pageConfig) return null;
408
+ return {
409
+ key: this.currentKey,
410
+ name: pageConfig.name,
411
+ type: pageConfig.type,
412
+ slug: pageConfig.slug || this.currentKey,
413
+ index: this.pageKeys.indexOf(this.currentKey)
414
+ };
415
+ }
416
+ /** Build the full URL path for a page key. */
417
+ getPageUrl(pageKey) {
418
+ const pageConfig = this.config.pages?.[pageKey];
419
+ const slug = pageConfig?.slug || pageKey;
420
+ return this.basePath ? `${this.basePath}/${slug}` : `/${slug}`;
421
+ }
422
+ /** Resolve a page key from a URL slug. */
423
+ resolveSlug(slug) {
424
+ const pages = this.config.pages ?? {};
425
+ if (pages[slug]) return slug;
426
+ for (const [key, config] of Object.entries(pages)) {
427
+ if (config.slug === slug) return key;
428
+ }
429
+ return null;
430
+ }
431
+ getPageHistory() {
432
+ return [...this.history];
433
+ }
434
+ getProgress() {
435
+ const total = this.calculateExpectedPathLength();
436
+ const current = this.history.length + 1;
437
+ return {
438
+ current,
439
+ total,
440
+ percentage: total > 0 ? Math.min(100, Math.round(current / total * 100)) : 0
441
+ };
442
+ }
443
+ // ── Navigation ───────────────────────────────────────
444
+ /**
445
+ * Evaluate routes for the current page and navigate to the first matching target.
446
+ * Returns the new page key, or null if no route matched.
447
+ */
448
+ goToNextPage(variables) {
449
+ const routes = this.config.routes?.[this.currentKey];
450
+ if (!routes || routes.length === 0) return null;
451
+ for (const route of routes) {
452
+ if (!route.when || evaluateCondition(route.when, variables)) {
453
+ return this.navigateTo(route.to);
454
+ }
455
+ }
456
+ return null;
457
+ }
458
+ /** Navigate to a specific page by key. */
459
+ goToPage(pageKey) {
460
+ if (!this.config.pages?.[pageKey]) return null;
461
+ return this.navigateTo(pageKey);
462
+ }
463
+ /** Go back to the previous page in history. */
464
+ goBack() {
465
+ const previousKey = this.history.pop();
466
+ if (!previousKey) return null;
467
+ this.currentKey = previousKey;
468
+ this.notify();
469
+ return previousKey;
470
+ }
471
+ /** Set the current page directly (used for deep-link/reload restore). */
472
+ setCurrentPage(pageKey) {
473
+ if (this.config.pages?.[pageKey] && this.currentKey !== pageKey) {
474
+ this.currentKey = pageKey;
475
+ this.notify();
476
+ }
477
+ }
478
+ navigateTo(pageKey) {
479
+ this.history.push(this.currentKey);
480
+ this.currentKey = pageKey;
481
+ this.notify();
482
+ return pageKey;
483
+ }
484
+ calculateExpectedPathLength() {
485
+ const visited = /* @__PURE__ */ new Set();
486
+ let current = this.initialKey || null;
487
+ let length = 0;
488
+ while (current && !visited.has(current)) {
489
+ visited.add(current);
490
+ length++;
491
+ const routes = this.config.routes?.[current];
492
+ if (!routes || routes.length === 0) break;
493
+ const fallback = routes[routes.length - 1];
494
+ current = fallback?.to ?? null;
495
+ }
496
+ return length;
497
+ }
498
+ };
499
+
500
+ // src/runtime/tracker.ts
501
+ function getCookieValue(name) {
502
+ if (typeof document === "undefined") return null;
503
+ const match = document.cookie.match(
504
+ new RegExp("(?:^|; )" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)")
505
+ );
506
+ return match ? decodeURIComponent(match[1]) : null;
507
+ }
508
+ function collectAdAttribution() {
509
+ if (typeof window === "undefined") return {};
510
+ const attribution = {};
511
+ const params = new URLSearchParams(window.location.search);
512
+ const fbp = getCookieValue("_fbp");
513
+ if (fbp) attribution.fbp = fbp;
514
+ const fbc = getCookieValue("_fbc");
515
+ const fbclid = params.get("fbclid");
516
+ if (fbc) {
517
+ attribution.fbc = fbc;
518
+ } else if (fbclid) {
519
+ attribution.fbc = `fb.1.${Date.now()}.${fbclid}`;
520
+ }
521
+ const ttp = getCookieValue("_ttp");
522
+ if (ttp) attribution.ttp = ttp;
523
+ const ttclid = params.get("ttclid");
524
+ if (ttclid) attribution.ttclid = ttclid;
525
+ for (const key of ["gclid", "wbraid", "gbraid"]) {
526
+ const val = params.get(key);
527
+ if (val) attribution[key] = val;
528
+ }
529
+ return attribution;
530
+ }
531
+ function generateEventId() {
532
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
533
+ return crypto.randomUUID();
534
+ }
535
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
536
+ const r = Math.random() * 16 | 0;
537
+ const v = c === "x" ? r : r & 3 | 8;
538
+ return v.toString(16);
539
+ });
540
+ }
541
+ function collectMetadata() {
542
+ if (typeof window === "undefined") return {};
543
+ const screen = `${window.screen.width}x${window.screen.height}`;
544
+ const language = navigator.language;
545
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
546
+ const referrer = document.referrer || void 0;
547
+ const userAgent = navigator.userAgent.toLowerCase();
548
+ let device = "desktop";
549
+ if (/mobile|android|iphone|ipod|blackberry|windows phone/.test(userAgent)) {
550
+ device = "mobile";
551
+ } else if (/tablet|ipad/.test(userAgent)) {
552
+ device = "tablet";
553
+ }
554
+ let browser = "unknown";
555
+ if (userAgent.includes("firefox")) browser = "Firefox";
556
+ else if (userAgent.includes("safari") && !userAgent.includes("chrome")) browser = "Safari";
557
+ else if (userAgent.includes("chrome")) browser = "Chrome";
558
+ else if (userAgent.includes("edge")) browser = "Edge";
559
+ let os = "unknown";
560
+ if (userAgent.includes("win")) os = "Windows";
561
+ else if (userAgent.includes("mac")) os = "macOS";
562
+ else if (userAgent.includes("linux")) os = "Linux";
563
+ else if (userAgent.includes("android")) os = "Android";
564
+ else if (/ios|iphone|ipad/.test(userAgent)) os = "iOS";
565
+ return { device, browser, os, screen, language, timezone, referrer };
566
+ }
567
+ var COOKIE_MAX_AGE_DAYS = 30;
568
+ function getSessionCookie(campaignSlug) {
569
+ if (typeof document === "undefined") return null;
570
+ const name = `fs_${campaignSlug}=`;
571
+ const cookies = document.cookie.split(";");
572
+ for (const cookie of cookies) {
573
+ const trimmed = cookie.trim();
574
+ if (trimmed.startsWith(name)) {
575
+ return trimmed.substring(name.length);
576
+ }
577
+ }
578
+ return null;
579
+ }
580
+ function setSessionCookie(campaignSlug, sessionId) {
581
+ if (typeof document === "undefined") return;
582
+ const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
583
+ document.cookie = `fs_${campaignSlug}=${sessionId}; path=/; max-age=${maxAge}; SameSite=Lax`;
584
+ }
585
+ var SAVABLE_PREFIXES = ["query.", "data.", "answers.", "user."];
586
+ function filterSavable(variables) {
587
+ const result = {};
588
+ for (const key of Object.keys(variables)) {
589
+ if (SAVABLE_PREFIXES.some((p) => key.startsWith(p))) {
590
+ result[key] = variables[key];
591
+ }
592
+ }
593
+ return result;
594
+ }
595
+ var VariablePersistence = class {
596
+ constructor(save, DEBOUNCE_MS = 2e3, MAX_DELAY_MS = 5e3) {
597
+ this.save = save;
598
+ this.DEBOUNCE_MS = DEBOUNCE_MS;
599
+ this.MAX_DELAY_MS = MAX_DELAY_MS;
600
+ this.variables = null;
601
+ this.debounceTimer = null;
602
+ this.maxDelayTimer = null;
603
+ }
604
+ update(variables, canSave = true) {
605
+ this.variables = variables;
606
+ if (canSave) this.scheduleSave();
607
+ }
608
+ /** Get the filtered savable variables (for attaching to events). */
609
+ getSavable() {
610
+ if (!this.variables) return null;
611
+ return filterSavable(this.variables);
612
+ }
613
+ async flush() {
614
+ this.clearTimers();
615
+ if (this.variables) {
616
+ await this.save(filterSavable(this.variables));
617
+ }
618
+ }
619
+ destroy() {
620
+ this.clearTimers();
621
+ }
622
+ scheduleSave() {
623
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
624
+ this.debounceTimer = setTimeout(() => this.executeSave(), this.DEBOUNCE_MS);
625
+ if (!this.maxDelayTimer) {
626
+ this.maxDelayTimer = setTimeout(() => this.executeSave(), this.MAX_DELAY_MS);
627
+ }
628
+ }
629
+ executeSave() {
630
+ this.clearTimers();
631
+ if (this.variables) {
632
+ this.save(filterSavable(this.variables)).catch((error) => {
633
+ console.error("[AppFunnel] Variable save failed:", error);
634
+ });
635
+ }
636
+ }
637
+ clearTimers() {
638
+ if (this.debounceTimer) {
639
+ clearTimeout(this.debounceTimer);
640
+ this.debounceTimer = null;
641
+ }
642
+ if (this.maxDelayTimer) {
643
+ clearTimeout(this.maxDelayTimer);
644
+ this.maxDelayTimer = null;
645
+ }
646
+ }
647
+ };
648
+ var API_BASE_URL = "https://api.appfunnel.net";
649
+ var FunnelTracker = class {
650
+ constructor() {
651
+ this.campaignId = null;
652
+ this.funnelId = null;
653
+ this.campaignSlug = null;
654
+ this.sessionId = null;
655
+ this.experimentId = null;
656
+ this.initialized = false;
657
+ this.customerId = null;
658
+ this.adAttribution = {};
659
+ // Page time tracking
660
+ this.currentPageId = null;
661
+ this.pageStartTime = null;
662
+ this.pageActiveTime = 0;
663
+ this.lastActiveStart = 0;
664
+ this.isPageVisible = true;
665
+ this.handleVisibilityChange = () => {
666
+ if (document.visibilityState === "hidden") {
667
+ if (this.isPageVisible && this.lastActiveStart > 0) {
668
+ this.pageActiveTime += Date.now() - this.lastActiveStart;
669
+ }
670
+ this.isPageVisible = false;
671
+ } else {
672
+ this.isPageVisible = true;
673
+ this.lastActiveStart = Date.now();
674
+ }
675
+ };
676
+ this.handleBeforeUnload = () => {
677
+ if (!this.currentPageId || !this.pageStartTime || !this.initialized || !this.campaignId) return;
678
+ const durationMs = Date.now() - this.pageStartTime;
679
+ const activeMs = this.calculateActiveTime();
680
+ const eventId = generateEventId();
681
+ const payload = JSON.stringify({
682
+ campaignId: this.campaignId,
683
+ funnelId: this.funnelId || void 0,
684
+ eventId,
685
+ sessionId: this.sessionId || void 0,
686
+ experimentId: this.experimentId || void 0,
687
+ event: "page.exit",
688
+ data: { pageId: this.currentPageId, durationMs, activeMs },
689
+ userData: this.variablePersistence.getSavable() || void 0,
690
+ metadata: collectMetadata(),
691
+ adAttribution: Object.keys(this.adAttribution).length > 0 ? this.adAttribution : void 0
692
+ });
693
+ navigator.sendBeacon(`${API_BASE_URL}/campaign/${this.campaignId}/event`, payload);
694
+ };
695
+ this.variablePersistence = new VariablePersistence((data) => this.updateUserData(data));
696
+ }
697
+ // ── Session management ──────────────────────────────
698
+ init(campaignId, funnelId, campaignSlug, experimentId) {
699
+ if (typeof window === "undefined") return;
700
+ this.campaignId = campaignId;
701
+ this.funnelId = funnelId;
702
+ this.campaignSlug = campaignSlug || null;
703
+ this.experimentId = experimentId || null;
704
+ this.initialized = true;
705
+ this.adAttribution = collectAdAttribution();
706
+ if (campaignSlug) {
707
+ const cookieSessionId = getSessionCookie(campaignSlug);
708
+ if (cookieSessionId) {
709
+ this.sessionId = cookieSessionId;
710
+ return;
711
+ }
712
+ }
713
+ const storedSessionId = localStorage.getItem(`campaign_session_${campaignId}`);
714
+ if (storedSessionId) {
715
+ this.sessionId = storedSessionId;
716
+ }
717
+ }
718
+ getSessionId() {
719
+ return this.sessionId;
720
+ }
721
+ getCustomerId() {
722
+ return this.customerId;
723
+ }
724
+ setSessionId(sessionId) {
725
+ this.persistSessionId(sessionId);
726
+ }
727
+ persistSessionId(sessionId) {
728
+ this.sessionId = sessionId;
729
+ if (this.campaignSlug) {
730
+ setSessionCookie(this.campaignSlug, sessionId);
731
+ }
732
+ if (this.campaignId) {
733
+ localStorage.setItem(`campaign_session_${this.campaignId}`, sessionId);
734
+ }
735
+ }
736
+ async track(event, data, userData) {
737
+ const eventId = data?.eventId || generateEventId();
738
+ if (!this.initialized || !this.campaignId) {
739
+ console.warn("[AppFunnel] Tracker not initialized. Call init() first.");
740
+ return eventId;
741
+ }
742
+ if (typeof window === "undefined") return eventId;
743
+ if (globalThis.__APPFUNNEL_DEV__) {
744
+ console.log(`[AppFunnel] track: ${event}`, data || "");
745
+ return eventId;
746
+ }
747
+ try {
748
+ const metadata = collectMetadata();
749
+ const response = await fetch(
750
+ `${API_BASE_URL}/campaign/${this.campaignId}/event`,
751
+ {
752
+ method: "POST",
753
+ headers: { "Content-Type": "application/json" },
754
+ body: JSON.stringify({
755
+ campaignId: this.campaignId,
756
+ funnelId: this.funnelId || void 0,
757
+ eventId,
758
+ sessionId: this.sessionId || void 0,
759
+ event,
760
+ data,
761
+ userData: userData || void 0,
762
+ metadata,
763
+ adAttribution: Object.keys(this.adAttribution).length > 0 ? this.adAttribution : void 0,
764
+ experimentId: this.experimentId || void 0
765
+ })
766
+ }
767
+ );
768
+ if (!response.ok) {
769
+ console.error("[AppFunnel] Failed to track event:", response.statusText);
770
+ return eventId;
771
+ }
772
+ const result = await response.json();
773
+ if (result.success && result.sessionId) {
774
+ this.persistSessionId(result.sessionId);
775
+ if (result.customerId && result.customerId !== this.customerId) {
776
+ this.customerId = result.customerId;
777
+ }
778
+ }
779
+ } catch (error) {
780
+ console.error("[AppFunnel] Error tracking event:", error);
781
+ }
782
+ return eventId;
783
+ }
784
+ /** Identify a user by email — fires user.registered event */
785
+ async identify(email) {
786
+ await this.track("user.registered", { email });
787
+ }
788
+ async updateUserData(userData) {
789
+ if (!this.sessionId || typeof window === "undefined") return;
790
+ try {
791
+ const response = await fetch(
792
+ `${API_BASE_URL}/campaign/${this.campaignId}/event/session/data`,
793
+ {
794
+ method: "POST",
795
+ headers: { "Content-Type": "application/json" },
796
+ body: JSON.stringify({ sessionId: this.sessionId, userData })
797
+ }
798
+ );
799
+ if (!response.ok) {
800
+ console.error("[AppFunnel] Failed to update user data:", response.statusText);
801
+ }
802
+ } catch (error) {
803
+ console.error("[AppFunnel] Error updating user data:", error);
804
+ }
805
+ }
806
+ // ── Variable persistence ────────────────────────────
807
+ setCurrentVariables(variables) {
808
+ this.variablePersistence.update(variables, !!this.sessionId);
809
+ }
810
+ async flushVariables() {
811
+ if (!this.sessionId) return;
812
+ await this.variablePersistence.flush();
813
+ }
814
+ // ── Page time tracking ──────────────────────────────
815
+ startPageTracking(pageId) {
816
+ if (typeof window === "undefined") return;
817
+ this.stopPageTracking();
818
+ this.currentPageId = pageId;
819
+ this.pageStartTime = Date.now();
820
+ this.pageActiveTime = 0;
821
+ this.lastActiveStart = Date.now();
822
+ this.isPageVisible = document.visibilityState === "visible";
823
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
824
+ window.addEventListener("beforeunload", this.handleBeforeUnload);
825
+ }
826
+ stopPageTracking() {
827
+ if (!this.currentPageId || !this.pageStartTime) return;
828
+ const durationMs = Date.now() - this.pageStartTime;
829
+ const activeMs = this.calculateActiveTime();
830
+ if (durationMs >= 50) {
831
+ this.track(
832
+ "page.exit",
833
+ { pageId: this.currentPageId, durationMs, activeMs },
834
+ this.variablePersistence.getSavable() || void 0
835
+ );
836
+ }
837
+ this.cleanupPageTracking();
838
+ }
839
+ calculateActiveTime() {
840
+ let activeTime = this.pageActiveTime;
841
+ if (this.isPageVisible && this.lastActiveStart > 0) {
842
+ activeTime += Date.now() - this.lastActiveStart;
843
+ }
844
+ return activeTime;
845
+ }
846
+ cleanupPageTracking() {
847
+ if (typeof window === "undefined") return;
848
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
849
+ window.removeEventListener("beforeunload", this.handleBeforeUnload);
850
+ this.currentPageId = null;
851
+ this.pageStartTime = null;
852
+ this.pageActiveTime = 0;
853
+ this.lastActiveStart = 0;
854
+ this.isPageVisible = true;
855
+ }
856
+ // ── Lifecycle ───────────────────────────────────────
857
+ reset() {
858
+ this.variablePersistence.destroy();
859
+ if (this.campaignId && typeof window !== "undefined") {
860
+ localStorage.removeItem(`campaign_session_${this.campaignId}`);
861
+ }
862
+ this.sessionId = null;
863
+ this.customerId = null;
864
+ this.cleanupPageTracking();
865
+ }
866
+ };
867
+
868
+ // src/runtime/products.ts
869
+ function formatPrice(amount, currency) {
870
+ try {
871
+ return new Intl.NumberFormat("en-US", {
872
+ style: "currency",
873
+ currency: currency.toUpperCase()
874
+ }).format(amount);
875
+ } catch {
876
+ return `${currency.toUpperCase()} ${amount.toFixed(2)}`;
877
+ }
878
+ }
879
+ function calculatePeriodDays(interval, intervalCount) {
880
+ const map = { day: 1, week: 7, month: 30, year: 365, one_time: 0 };
881
+ return (map[interval] || 0) * intervalCount;
882
+ }
883
+ function calculatePeriodMonths(interval, intervalCount) {
884
+ const map = { day: 1 / 30, week: 1 / 4, month: 1, year: 12, one_time: 0 };
885
+ return (map[interval] || 0) * intervalCount;
886
+ }
887
+ function calculatePeriodWeeks(interval, intervalCount) {
888
+ const map = { day: 1 / 7, week: 1, month: 4, year: 52, one_time: 0 };
889
+ return (map[interval] || 0) * intervalCount;
890
+ }
891
+ function getIntervalLabel(interval, intervalCount) {
892
+ if (interval === "one_time") return { period: "one-time", periodly: "one-time" };
893
+ if (interval === "month" && intervalCount === 3) return { period: "quarter", periodly: "quarterly" };
894
+ if (interval === "month" && intervalCount === 6) return { period: "6 months", periodly: "semiannually" };
895
+ if (interval === "week" && intervalCount === 2) return { period: "2 weeks", periodly: "biweekly" };
896
+ if (intervalCount === 1) {
897
+ const periodlyMap = { day: "daily", week: "weekly", month: "monthly", year: "yearly" };
898
+ return { period: interval, periodly: periodlyMap[interval] || interval };
899
+ }
900
+ return {
901
+ period: `${intervalCount} ${interval}s`,
902
+ periodly: `every ${intervalCount} ${interval}s`
903
+ };
904
+ }
905
+ function getCurrencySymbol(currency) {
906
+ try {
907
+ const parts = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).formatToParts(0);
908
+ return parts.find((p) => p.type === "currency")?.value || currency.toUpperCase();
909
+ } catch {
910
+ return currency.toUpperCase();
911
+ }
912
+ }
913
+ function buildRuntimeProduct(product, priceData, trialPriceData) {
914
+ if (!priceData) {
915
+ const c = "USD";
916
+ const f2 = (n) => formatPrice(n, c);
917
+ const fallbackTrialDays = product.trialDays || 0;
918
+ return {
919
+ id: product.id,
920
+ name: product.name,
921
+ storePriceId: product.storePriceId,
922
+ price: f2(0),
923
+ rawPrice: 0,
924
+ monthlyPrice: f2(0),
925
+ dailyPrice: f2(0),
926
+ weeklyPrice: f2(0),
927
+ yearlyPrice: f2(0),
928
+ period: "one_time",
929
+ periodly: "one-time",
930
+ periodDays: 0,
931
+ periodMonths: 0,
932
+ periodWeeks: 0,
933
+ currencyCode: c,
934
+ currencySymbol: getCurrencySymbol(c),
935
+ hasTrial: fallbackTrialDays > 0,
936
+ trialDays: fallbackTrialDays,
937
+ paidTrial: false,
938
+ trialRawPrice: 0,
939
+ trialPrice: f2(0),
940
+ trialDailyPrice: f2(0),
941
+ trialCurrencyCode: c,
942
+ trialStorePriceId: product.trialStorePriceId || "",
943
+ stripePriceId: "",
944
+ stripeProductId: "",
945
+ paddlePriceId: "",
946
+ paddleProductId: "",
947
+ displayName: product.name
948
+ };
949
+ }
950
+ const rawPrice = priceData.amount / 100;
951
+ const currency = priceData.currency.toUpperCase();
952
+ const f = (n) => formatPrice(n, currency);
953
+ const interval = priceData.interval || "one_time";
954
+ const intervalCount = priceData.intervalCount || 1;
955
+ const periodDays = calculatePeriodDays(interval, intervalCount);
956
+ const periodMonths = calculatePeriodMonths(interval, intervalCount);
957
+ const periodWeeks = calculatePeriodWeeks(interval, intervalCount);
958
+ const dailyPrice = periodDays > 0 ? rawPrice / periodDays : rawPrice;
959
+ const weeklyPrice = periodWeeks > 0 ? rawPrice / periodWeeks : rawPrice * 7;
960
+ const monthlyPrice = periodMonths > 0 ? rawPrice / periodMonths : rawPrice * 30;
961
+ const yearlyPrice = periodMonths > 0 ? rawPrice / periodMonths * 12 : rawPrice * 365;
962
+ const { period, periodly } = getIntervalLabel(interval, intervalCount);
963
+ const trialDays = product.trialDays || 0;
964
+ const hasTrial = trialDays > 0;
965
+ const trialRawPrice = trialPriceData ? trialPriceData.amount / 100 : 0;
966
+ const trialCurrency = trialPriceData ? trialPriceData.currency.toUpperCase() : currency;
967
+ const ft = (n) => formatPrice(n, trialCurrency);
968
+ const trialDailyPrice = trialDays > 0 ? trialRawPrice / trialDays : 0;
969
+ return {
970
+ id: product.id,
971
+ name: product.name,
972
+ storePriceId: product.storePriceId,
973
+ price: f(rawPrice),
974
+ rawPrice,
975
+ monthlyPrice: f(monthlyPrice),
976
+ dailyPrice: f(dailyPrice),
977
+ weeklyPrice: f(weeklyPrice),
978
+ yearlyPrice: f(yearlyPrice),
979
+ period,
980
+ periodly,
981
+ periodDays,
982
+ periodMonths,
983
+ periodWeeks,
984
+ currencyCode: currency,
985
+ currencySymbol: getCurrencySymbol(currency),
986
+ hasTrial,
987
+ trialDays,
988
+ paidTrial: hasTrial && trialRawPrice > 0,
989
+ trialRawPrice,
990
+ trialPrice: ft(trialRawPrice),
991
+ trialDailyPrice: ft(trialDailyPrice),
992
+ trialCurrencyCode: trialCurrency,
993
+ trialStorePriceId: product.trialStorePriceId || "",
994
+ stripePriceId: priceData.stripePriceId || "",
995
+ stripeProductId: priceData.stripeProductId || "",
996
+ paddlePriceId: priceData.paddlePriceId || "",
997
+ paddleProductId: priceData.paddleProductId || "",
998
+ displayName: priceData.displayName || priceData.priceName || priceData.name || product.name
999
+ };
1000
+ }
1001
+ function buildRuntimeProducts(products, priceDataMap) {
1002
+ return products.map((product) => {
1003
+ const priceData = priceDataMap.get(product.storePriceId);
1004
+ const trialPriceData = product.trialStorePriceId ? priceDataMap.get(product.trialStorePriceId) : void 0;
1005
+ return buildRuntimeProduct(product, priceData, trialPriceData);
1006
+ });
1007
+ }
1008
+
1009
+ // src/runtime/systemVariables.ts
1010
+ var cachedBrowserInfo = null;
1011
+ function detectBrowserInfo() {
1012
+ if (cachedBrowserInfo) return cachedBrowserInfo;
1013
+ if (typeof window === "undefined") {
1014
+ return {
1015
+ userAgent: "unknown",
1016
+ browserName: "Unknown",
1017
+ browserVersion: "0",
1018
+ deviceType: "desktop",
1019
+ isMobile: false,
1020
+ isTablet: false,
1021
+ os: "Unknown",
1022
+ screenWidth: 0,
1023
+ screenHeight: 0,
1024
+ viewportWidth: 0,
1025
+ viewportHeight: 0,
1026
+ language: "en",
1027
+ timezone: "UTC",
1028
+ colorDepth: 24,
1029
+ pixelRatio: 1,
1030
+ cookieEnabled: true,
1031
+ online: true
1032
+ };
1033
+ }
1034
+ const ua = navigator.userAgent;
1035
+ const uaLower = ua.toLowerCase();
1036
+ let browserName = "Unknown";
1037
+ let browserVersion = "0";
1038
+ if (uaLower.includes("firefox")) {
1039
+ browserName = "Firefox";
1040
+ browserVersion = extractVersion(ua, /Firefox\/(\d+[\d.]*)/);
1041
+ } else if (uaLower.includes("edg")) {
1042
+ browserName = "Edge";
1043
+ browserVersion = extractVersion(ua, /Edg\/(\d+[\d.]*)/);
1044
+ } else if (uaLower.includes("chrome") && !uaLower.includes("edg")) {
1045
+ browserName = "Chrome";
1046
+ browserVersion = extractVersion(ua, /Chrome\/(\d+[\d.]*)/);
1047
+ } else if (uaLower.includes("safari") && !uaLower.includes("chrome")) {
1048
+ browserName = "Safari";
1049
+ browserVersion = extractVersion(ua, /Version\/(\d+[\d.]*)/);
1050
+ }
1051
+ let os = "Unknown";
1052
+ if (uaLower.includes("win")) os = "Windows";
1053
+ else if (uaLower.includes("mac")) os = "macOS";
1054
+ else if (uaLower.includes("linux") && !uaLower.includes("android")) os = "Linux";
1055
+ else if (uaLower.includes("android")) os = "Android";
1056
+ else if (/iphone|ipad|ipod/.test(uaLower)) os = "iOS";
1057
+ let deviceType = "desktop";
1058
+ if (/mobile|android(?!.*tablet)|iphone|ipod/.test(uaLower)) deviceType = "mobile";
1059
+ else if (/tablet|ipad/.test(uaLower)) deviceType = "tablet";
1060
+ cachedBrowserInfo = {
1061
+ userAgent: ua,
1062
+ browserName,
1063
+ browserVersion,
1064
+ deviceType,
1065
+ isMobile: deviceType === "mobile",
1066
+ isTablet: deviceType === "tablet",
1067
+ os,
1068
+ screenWidth: window.screen?.width || 0,
1069
+ screenHeight: window.screen?.height || 0,
1070
+ viewportWidth: window.innerWidth || 0,
1071
+ viewportHeight: window.innerHeight || 0,
1072
+ language: navigator.language || "en",
1073
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
1074
+ colorDepth: window.screen?.colorDepth || 24,
1075
+ pixelRatio: window.devicePixelRatio || 1,
1076
+ cookieEnabled: navigator.cookieEnabled ?? true,
1077
+ online: navigator.onLine ?? true
1078
+ };
1079
+ return cachedBrowserInfo;
1080
+ }
1081
+ function extractVersion(ua, regex) {
1082
+ const match = ua.match(regex);
1083
+ return match?.[1] || "0";
1084
+ }
1085
+ function computeSystemVariables(context) {
1086
+ const browser = detectBrowserInfo();
1087
+ const now = Date.now();
1088
+ return {
1089
+ // Session
1090
+ "session.startedAt": context.sessionStartTime,
1091
+ // Page
1092
+ "page.currentId": context.currentPageKey,
1093
+ "page.currentIndex": context.pageHistory.length,
1094
+ "page.current": context.pageHistory.length + 1,
1095
+ "page.total": context.totalPages,
1096
+ "page.progressPercentage": context.totalPages > 0 ? Math.min(100, Math.round((context.pageHistory.length + 1) / context.totalPages * 100)) : 0,
1097
+ "page.startedAt": context.pageStartTime || now,
1098
+ // Device
1099
+ "device.isMobile": browser.isMobile,
1100
+ "device.isTablet": browser.isTablet,
1101
+ "device.type": browser.deviceType,
1102
+ "device.screenWidth": browser.screenWidth,
1103
+ "device.screenHeight": browser.screenHeight,
1104
+ "device.viewportWidth": browser.viewportWidth,
1105
+ "device.viewportHeight": browser.viewportHeight,
1106
+ "device.colorDepth": browser.colorDepth,
1107
+ "device.pixelRatio": browser.pixelRatio,
1108
+ // Browser
1109
+ "browser.userAgent": browser.userAgent,
1110
+ "browser.name": browser.browserName,
1111
+ "browser.version": browser.browserVersion,
1112
+ "browser.cookieEnabled": browser.cookieEnabled,
1113
+ "browser.online": browser.online,
1114
+ "browser.language": browser.language,
1115
+ // OS
1116
+ "os.name": browser.os,
1117
+ "os.timezone": browser.timezone,
1118
+ // Metadata
1119
+ "metadata.webfunnelId": context.funnelId,
1120
+ "metadata.campaignId": context.campaignId
1121
+ };
1122
+ }
1123
+
1124
+ // src/runtime/i18n.ts
1125
+ var I18n = class {
1126
+ constructor(defaultLocale = "en") {
1127
+ this.translations = {};
1128
+ this.listeners = /* @__PURE__ */ new Set();
1129
+ this.locale = defaultLocale;
1130
+ this.fallbackLocale = defaultLocale;
1131
+ }
1132
+ /** Load all translations at once */
1133
+ load(translations) {
1134
+ this.translations = translations;
1135
+ }
1136
+ getLocale() {
1137
+ return this.locale;
1138
+ }
1139
+ setLocale(locale) {
1140
+ if (this.locale === locale) return;
1141
+ this.locale = locale;
1142
+ this.notify();
1143
+ }
1144
+ getAvailableLocales() {
1145
+ return Object.keys(this.translations);
1146
+ }
1147
+ /**
1148
+ * Translate a key with optional interpolation.
1149
+ *
1150
+ * Supports dot notation for nested keys and `{{var}}` interpolation:
1151
+ * t('checkout.title')
1152
+ * t('welcome', { name: 'John' }) → "Hello, John"
1153
+ */
1154
+ t(key, params) {
1155
+ let value = this.resolve(this.locale, key) ?? this.resolve(this.fallbackLocale, key);
1156
+ if (value === void 0) return key;
1157
+ if (params) {
1158
+ value = value.replace(/\{\{(\w+)\}\}/g, (_, name) => {
1159
+ return params[name] !== void 0 ? String(params[name]) : `{{${name}}}`;
1160
+ });
1161
+ }
1162
+ return value;
1163
+ }
1164
+ subscribe(listener) {
1165
+ this.listeners.add(listener);
1166
+ return () => this.listeners.delete(listener);
1167
+ }
1168
+ resolve(locale, key) {
1169
+ const dict = this.translations[locale];
1170
+ if (!dict) return void 0;
1171
+ if (dict[key] !== void 0) return dict[key];
1172
+ return void 0;
1173
+ }
1174
+ notify() {
1175
+ this.listeners.forEach((l) => l());
1176
+ }
1177
+ };
1178
+ var FunnelContext = createContext(null);
1179
+ function useFunnelContext() {
1180
+ const ctx = useContext(FunnelContext);
1181
+ if (!ctx) {
1182
+ throw new Error("useFunnelContext must be used within a <FunnelProvider>");
1183
+ }
1184
+ return ctx;
1185
+ }
1186
+ function FunnelProvider({
1187
+ config,
1188
+ children,
1189
+ sessionData,
1190
+ priceData,
1191
+ campaignSlug,
1192
+ basePath,
1193
+ initialPage,
1194
+ translations
1195
+ }) {
1196
+ const campaignId = sessionData?.campaignId || config.projectId || "";
1197
+ const funnelId = sessionData?.funnelId || config.projectId || "";
1198
+ const storeRef = useRef(null);
1199
+ const routerRef = useRef(null);
1200
+ const trackerRef = useRef(null);
1201
+ const eventBusRef = useRef(null);
1202
+ const i18nRef = useRef(null);
1203
+ if (!storeRef.current) {
1204
+ storeRef.current = createVariableStore(
1205
+ { responses: config.responses, queryParams: config.queryParams, data: config.data },
1206
+ sessionData?.variables
1207
+ );
1208
+ }
1209
+ if (!routerRef.current) {
1210
+ routerRef.current = new Router({
1211
+ config,
1212
+ initialPage,
1213
+ basePath,
1214
+ campaignSlug
1215
+ });
1216
+ }
1217
+ if (!trackerRef.current) {
1218
+ trackerRef.current = new FunnelTracker();
1219
+ }
1220
+ if (!i18nRef.current) {
1221
+ const i18n2 = new I18n(config.defaultLocale || "en");
1222
+ if (translations) i18n2.load(translations);
1223
+ if (typeof navigator !== "undefined") {
1224
+ const browserLang = navigator.language?.split("-")[0];
1225
+ if (browserLang && translations?.[browserLang]) {
1226
+ i18n2.setLocale(browserLang);
1227
+ }
1228
+ }
1229
+ i18nRef.current = i18n2;
1230
+ }
1231
+ const store = storeRef.current;
1232
+ const router = routerRef.current;
1233
+ const tracker = trackerRef.current;
1234
+ const i18n = i18nRef.current;
1235
+ const products = useMemo(() => {
1236
+ if (!config.products?.items || !priceData) return [];
1237
+ return buildRuntimeProducts(config.products.items, priceData);
1238
+ }, [config.products, priceData]);
1239
+ const defaultProductId = config.products?.defaultId || products[0]?.id || null;
1240
+ const selectedProductIdRef = useRef(defaultProductId);
1241
+ const selectProduct = useCallback((productId) => {
1242
+ selectedProductIdRef.current = productId;
1243
+ store.set("products.selectedProductId", productId);
1244
+ }, [store]);
1245
+ useEffect(() => {
1246
+ const eventBus = new AppFunnelEventBus(store, router, selectProduct);
1247
+ eventBus.attach();
1248
+ eventBusRef.current = eventBus;
1249
+ if (config.integrations && Object.keys(config.integrations).length > 0) {
1250
+ initializeIntegrations(config.integrations);
1251
+ }
1252
+ return () => {
1253
+ eventBus.destroy();
1254
+ eventBusRef.current = null;
1255
+ };
1256
+ }, []);
1257
+ useEffect(() => {
1258
+ tracker.init(campaignId, funnelId, campaignSlug, sessionData?.experimentId);
1259
+ if (sessionData?.sessionId) {
1260
+ tracker.setSessionId(sessionData.sessionId);
1261
+ }
1262
+ const timer = setTimeout(() => {
1263
+ const currentPage = router.getCurrentPage();
1264
+ tracker.track("funnel.start");
1265
+ if (currentPage) {
1266
+ tracker.track("page.view", {
1267
+ pageId: currentPage.key,
1268
+ pageKey: currentPage.key,
1269
+ pageName: currentPage.name,
1270
+ isInitial: true
1271
+ });
1272
+ tracker.startPageTracking(currentPage.key);
1273
+ }
1274
+ }, 50);
1275
+ return () => {
1276
+ clearTimeout(timer);
1277
+ tracker.stopPageTracking();
1278
+ tracker.flushVariables();
1279
+ };
1280
+ }, []);
1281
+ const sessionStartTime = useRef(Date.now());
1282
+ const pageStartTime = useRef(Date.now());
1283
+ useEffect(() => {
1284
+ const sysVars = computeSystemVariables({
1285
+ currentPageKey: router.getCurrentPage()?.key || "",
1286
+ pageHistory: router.getPageHistory(),
1287
+ pageStartTime: pageStartTime.current,
1288
+ sessionStartTime: sessionStartTime.current,
1289
+ totalPages: Object.keys(config.pages ?? {}).length,
1290
+ funnelId,
1291
+ campaignId
1292
+ });
1293
+ store.setMany(sysVars);
1294
+ if (defaultProductId) {
1295
+ store.set("products.selectedProductId", defaultProductId);
1296
+ }
1297
+ }, []);
1298
+ useEffect(() => {
1299
+ return store.subscribe(() => {
1300
+ tracker.setCurrentVariables(store.getState());
1301
+ });
1302
+ }, [store, tracker]);
1303
+ const contextValue = useMemo(() => ({
1304
+ config,
1305
+ variableStore: store,
1306
+ router,
1307
+ tracker,
1308
+ i18n,
1309
+ products,
1310
+ selectedProductId: selectedProductIdRef.current,
1311
+ selectProduct,
1312
+ funnelId,
1313
+ campaignId,
1314
+ sessionId: tracker.getSessionId()
1315
+ }), [config, store, router, tracker, i18n, products, selectProduct, funnelId, campaignId]);
1316
+ return /* @__PURE__ */ jsx(FunnelContext.Provider, { value: contextValue, children });
1317
+ }
1318
+
1319
+ export { FunnelProvider, __require, registerIntegration, useFunnelContext };
1320
+ //# sourceMappingURL=chunk-H3KHXZSI.js.map
1321
+ //# sourceMappingURL=chunk-H3KHXZSI.js.map