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