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