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