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

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