@appfunnel-dev/sdk 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
- import { createContext, useCallback, useSyncExternalStore, useMemo, useState, useRef, useEffect, useContext } from 'react';
2
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { useNavigation, useResponses } from './chunk-P4SLDMWY.js';
2
+ export { useNavigation, useResponse, useResponses } from './chunk-P4SLDMWY.js';
3
+ import { useFunnelContext } from './chunk-H3KHXZSI.js';
4
+ export { FunnelProvider, registerIntegration } from './chunk-H3KHXZSI.js';
5
+ import { forwardRef, useState, useRef, useEffect, useCallback, useImperativeHandle, useMemo, useSyncExternalStore } from 'react';
3
6
  import { loadStripe } from '@stripe/stripe-js';
4
- import { Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
7
+ import { useStripe, useElements, PaymentElement, EmbeddedCheckoutProvider, EmbeddedCheckout, Elements } from '@stripe/react-stripe-js';
8
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
9
 
6
10
  // src/config.ts
7
11
  function defineConfig(config) {
@@ -10,23 +14,11 @@ function defineConfig(config) {
10
14
  function definePage(definition) {
11
15
  return definition;
12
16
  }
13
- function registerIntegration(id, loader) {
14
- }
15
- var FunnelContext = createContext(null);
16
- function useFunnelContext() {
17
- const ctx = useContext(FunnelContext);
18
- if (!ctx) {
19
- throw new Error("useFunnelContext must be used within a <FunnelProvider>");
20
- }
21
- return ctx;
22
- }
23
-
24
- // src/hooks/useVariable.ts
25
17
  function useVariable(id) {
26
18
  const { variableStore } = useFunnelContext();
27
19
  const subscribe = useCallback(
28
- (callback) => variableStore.subscribe(callback),
29
- [variableStore]
20
+ (callback) => variableStore.subscribe(callback, { keys: [id] }),
21
+ [variableStore, id]
30
22
  );
31
23
  const getSnapshot = useCallback(
32
24
  () => variableStore.get(id),
@@ -51,10 +43,106 @@ function useVariables() {
51
43
  );
52
44
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
53
45
  }
46
+
47
+ // src/utils/date.ts
48
+ function toISODate(input) {
49
+ if (!input || !input.trim()) return "";
50
+ const s = input.trim();
51
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
52
+ if (/^\d{4}-\d{2}-\d{2}T/.test(s)) return s.slice(0, 10);
53
+ const sepMatch = s.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
54
+ if (sepMatch) {
55
+ const a = parseInt(sepMatch[1], 10);
56
+ const b = parseInt(sepMatch[2], 10);
57
+ const year = sepMatch[3];
58
+ let month;
59
+ let day;
60
+ if (a > 12 && b <= 12) {
61
+ day = a;
62
+ month = b;
63
+ } else if (b > 12 && a <= 12) {
64
+ month = a;
65
+ day = b;
66
+ } else {
67
+ month = a;
68
+ day = b;
69
+ }
70
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
71
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
72
+ }
73
+ }
74
+ const ymdSlash = s.match(/^(\d{4})[/\-.](\d{1,2})[/\-.](\d{1,2})$/);
75
+ if (ymdSlash) {
76
+ const year = ymdSlash[1];
77
+ const month = parseInt(ymdSlash[2], 10);
78
+ const day = parseInt(ymdSlash[3], 10);
79
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
80
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
81
+ }
82
+ }
83
+ if (/^\d{8}$/.test(s)) {
84
+ const a = parseInt(s.slice(0, 2), 10);
85
+ const b = parseInt(s.slice(2, 4), 10);
86
+ const year = s.slice(4, 8);
87
+ let month;
88
+ let day;
89
+ if (a > 12 && b <= 12) {
90
+ day = a;
91
+ month = b;
92
+ } else {
93
+ month = a;
94
+ day = b;
95
+ }
96
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
97
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
98
+ }
99
+ }
100
+ throw new Error(
101
+ `[AppFunnel] Invalid date format: "${input}". Expected a date string like MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, or MMDDYYYY.`
102
+ );
103
+ }
104
+ function toISODateWithFormat(input, format) {
105
+ if (!input || !input.trim()) return "";
106
+ const s = input.trim();
107
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s) || /^\d{4}-\d{2}-\d{2}T/.test(s)) {
108
+ return s.slice(0, 10);
109
+ }
110
+ const digits = s.replace(/[^\d]/g, "");
111
+ if (format === "YYYY-MM-DD") {
112
+ const m = s.match(/^(\d{4})[/\-.](\d{1,2})[/\-.](\d{1,2})$/);
113
+ if (m) return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
114
+ if (digits.length === 8) {
115
+ return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
116
+ }
117
+ }
118
+ if (format === "MM/DD/YYYY") {
119
+ const m = s.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
120
+ if (m) return `${m[3]}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
121
+ if (digits.length === 8) {
122
+ return `${digits.slice(4, 8)}-${digits.slice(0, 2)}-${digits.slice(2, 4)}`;
123
+ }
124
+ }
125
+ if (format === "DD/MM/YYYY") {
126
+ const m = s.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
127
+ if (m) return `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
128
+ if (digits.length === 8) {
129
+ return `${digits.slice(4, 8)}-${digits.slice(2, 4)}-${digits.slice(0, 2)}`;
130
+ }
131
+ }
132
+ throw new Error(
133
+ `[AppFunnel] Invalid date format: "${input}". Expected format ${format} (e.g. ${{
134
+ "MM/DD/YYYY": "03/15/1990 or 03151990",
135
+ "DD/MM/YYYY": "15/03/1990 or 15031990",
136
+ "YYYY-MM-DD": "1990-03-15 or 19900315"
137
+ }[format]}).`
138
+ );
139
+ }
140
+
141
+ // src/hooks/useUser.ts
54
142
  function useUser() {
55
- const { variableStore, tracker } = useFunnelContext();
143
+ const { variableStore } = useFunnelContext();
56
144
  const subscribe = useCallback(
57
- (cb) => variableStore.subscribe(cb),
145
+ (cb) => variableStore.subscribe(cb, { prefix: "user." }),
58
146
  [variableStore]
59
147
  );
60
148
  const getSnapshot = useCallback(
@@ -71,24 +159,23 @@ function useUser() {
71
159
  dateOfBirth: variables["user.dateOfBirth"] || "",
72
160
  setEmail(email) {
73
161
  variableStore.set("user.email", email);
74
- tracker.identify(email);
75
162
  },
76
163
  setName(name) {
77
164
  variableStore.set("user.name", name);
78
165
  },
79
166
  setDateOfBirth(dateOfBirth) {
80
- variableStore.set("user.dateOfBirth", dateOfBirth);
167
+ variableStore.set("user.dateOfBirth", toISODate(dateOfBirth));
81
168
  }
82
169
  }),
83
- [variables, variableStore, tracker]
170
+ [variables, variableStore]
84
171
  );
85
172
  }
86
173
  function useUserProperty(field) {
87
174
  const { variableStore } = useFunnelContext();
88
175
  const key = `user.${field}`;
89
176
  const subscribe = useCallback(
90
- (cb) => variableStore.subscribe(cb),
91
- [variableStore]
177
+ (cb) => variableStore.subscribe(cb, { keys: [key] }),
178
+ [variableStore, key]
92
179
  );
93
180
  const getSnapshot = useCallback(
94
181
  () => variableStore.get(key) || "",
@@ -101,49 +188,28 @@ function useUserProperty(field) {
101
188
  );
102
189
  return [value, setValue];
103
190
  }
104
- function useResponse(key) {
191
+ function useDateOfBirth(format = "MM/DD/YYYY") {
105
192
  const { variableStore } = useFunnelContext();
106
- const prefixedKey = `answers.${key}`;
193
+ const key = "user.dateOfBirth";
107
194
  const subscribe = useCallback(
108
- (cb) => variableStore.subscribe(cb),
195
+ (cb) => variableStore.subscribe(cb, { keys: [key] }),
109
196
  [variableStore]
110
197
  );
111
198
  const getSnapshot = useCallback(
112
- () => variableStore.get(prefixedKey),
113
- [variableStore, prefixedKey]
199
+ () => variableStore.get(key) || "",
200
+ [variableStore]
114
201
  );
115
202
  const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
116
203
  const setValue = useCallback(
117
- (v) => variableStore.set(prefixedKey, v),
118
- [variableStore, prefixedKey]
204
+ (v) => variableStore.set(key, toISODateWithFormat(v, format)),
205
+ [variableStore, format]
119
206
  );
120
207
  return [value, setValue];
121
208
  }
122
- function useResponses() {
123
- const { variableStore } = useFunnelContext();
124
- const subscribe = useCallback(
125
- (cb) => variableStore.subscribe(cb),
126
- [variableStore]
127
- );
128
- const getSnapshot = useCallback(
129
- () => variableStore.getState(),
130
- [variableStore]
131
- );
132
- const variables = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
133
- return useMemo(() => {
134
- const result = {};
135
- for (const [key, value] of Object.entries(variables)) {
136
- if (key.startsWith("answers.")) {
137
- result[key.slice(8)] = value;
138
- }
139
- }
140
- return result;
141
- }, [variables]);
142
- }
143
209
  function useQueryParams() {
144
210
  const { variableStore } = useFunnelContext();
145
211
  const subscribe = useCallback(
146
- (cb) => variableStore.subscribe(cb),
212
+ (cb) => variableStore.subscribe(cb, { prefix: "query." }),
147
213
  [variableStore]
148
214
  );
149
215
  const getSnapshot = useCallback(
@@ -165,8 +231,8 @@ function useQueryParam(key) {
165
231
  const { variableStore } = useFunnelContext();
166
232
  const prefixedKey = `query.${key}`;
167
233
  const subscribe = useCallback(
168
- (cb) => variableStore.subscribe(cb),
169
- [variableStore]
234
+ (cb) => variableStore.subscribe(cb, { keys: [prefixedKey] }),
235
+ [variableStore, prefixedKey]
170
236
  );
171
237
  const getSnapshot = useCallback(
172
238
  () => variableStore.get(prefixedKey) || "",
@@ -178,8 +244,8 @@ function useData(key) {
178
244
  const { variableStore } = useFunnelContext();
179
245
  const prefixedKey = `data.${key}`;
180
246
  const subscribe = useCallback(
181
- (cb) => variableStore.subscribe(cb),
182
- [variableStore]
247
+ (cb) => variableStore.subscribe(cb, { keys: [prefixedKey] }),
248
+ [variableStore, prefixedKey]
183
249
  );
184
250
  const getSnapshot = useCallback(
185
251
  () => variableStore.get(prefixedKey),
@@ -250,99 +316,10 @@ function useTranslation() {
250
316
  const availableLocales = i18n.getAvailableLocales();
251
317
  return { t, locale, setLocale, availableLocales };
252
318
  }
253
- function useNavigation() {
254
- const { router, variableStore, tracker } = useFunnelContext();
255
- const subscribe = useCallback(
256
- (cb) => variableStore.subscribe(cb),
257
- [variableStore]
258
- );
259
- const getSnapshot = useCallback(
260
- () => variableStore.getState(),
261
- [variableStore]
262
- );
263
- const variables = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
264
- const goToNextPage = useCallback(() => {
265
- const previousPage = router.getCurrentPage();
266
- if (previousPage) {
267
- tracker.stopPageTracking();
268
- }
269
- const nextKey = router.goToNextPage(variables);
270
- if (nextKey) {
271
- const nextPage = router.getCurrentPage();
272
- if (nextPage) {
273
- tracker.track("page.view", {
274
- pageId: nextPage.key,
275
- pageKey: nextPage.key,
276
- pageName: nextPage.name
277
- });
278
- tracker.startPageTracking(nextPage.key);
279
- }
280
- variableStore.setMany({
281
- "page.currentId": nextKey,
282
- "page.currentIndex": router.getPageHistory().length,
283
- "page.current": router.getPageHistory().length + 1,
284
- "page.startedAt": Date.now(),
285
- "page.timeOnCurrent": 0
286
- });
287
- }
288
- }, [router, variables, tracker, variableStore]);
289
- const goBack = useCallback(() => {
290
- tracker.stopPageTracking();
291
- const prevKey = router.goBack();
292
- if (prevKey) {
293
- const page = router.getCurrentPage();
294
- if (page) {
295
- tracker.track("page.view", {
296
- pageId: page.key,
297
- pageKey: page.key,
298
- pageName: page.name
299
- });
300
- tracker.startPageTracking(page.key);
301
- }
302
- variableStore.setMany({
303
- "page.currentId": prevKey,
304
- "page.currentIndex": router.getPageHistory().length,
305
- "page.current": router.getPageHistory().length + 1,
306
- "page.startedAt": Date.now(),
307
- "page.timeOnCurrent": 0
308
- });
309
- }
310
- }, [router, tracker, variableStore]);
311
- const goToPage = useCallback((pageKey) => {
312
- tracker.stopPageTracking();
313
- const key = router.goToPage(pageKey);
314
- if (key) {
315
- const page = router.getCurrentPage();
316
- if (page) {
317
- tracker.track("page.view", {
318
- pageId: page.key,
319
- pageKey: page.key,
320
- pageName: page.name
321
- });
322
- tracker.startPageTracking(page.key);
323
- }
324
- variableStore.setMany({
325
- "page.currentId": key,
326
- "page.currentIndex": router.getPageHistory().length,
327
- "page.current": router.getPageHistory().length + 1,
328
- "page.startedAt": Date.now(),
329
- "page.timeOnCurrent": 0
330
- });
331
- }
332
- }, [router, tracker, variableStore]);
333
- return {
334
- goToNextPage,
335
- goBack,
336
- goToPage,
337
- currentPage: router.getCurrentPage(),
338
- pageHistory: router.getPageHistory(),
339
- progress: router.getProgress()
340
- };
341
- }
342
319
  function useProducts() {
343
320
  const { products, variableStore, selectProduct: ctxSelect } = useFunnelContext();
344
321
  const subscribe = useCallback(
345
- (cb) => variableStore.subscribe(cb),
322
+ (cb) => variableStore.subscribe(cb, { keys: ["products.selectedProductId"] }),
346
323
  [variableStore]
347
324
  );
348
325
  const getSnapshot = useCallback(
@@ -372,10 +349,19 @@ function useTracking() {
372
349
  );
373
350
  return { track, identify };
374
351
  }
352
+ var PAYMENT_KEYS = [
353
+ "card.last4",
354
+ "card.brand",
355
+ "card.expMonth",
356
+ "card.expYear",
357
+ "payment.loading",
358
+ "payment.error",
359
+ "payment.customerId"
360
+ ];
375
361
  function usePayment() {
376
- const { variableStore, tracker } = useFunnelContext();
362
+ const { variableStore } = useFunnelContext();
377
363
  const subscribe = useCallback(
378
- (cb) => variableStore.subscribe(cb),
364
+ (cb) => variableStore.subscribe(cb, { keys: PAYMENT_KEYS }),
379
365
  [variableStore]
380
366
  );
381
367
  const getSnapshot = useCallback(
@@ -388,14 +374,188 @@ function usePayment() {
388
374
  const brand = variables["card.brand"] || "";
389
375
  const expMonth = variables["card.expMonth"] || 0;
390
376
  const expYear = variables["card.expYear"] || 0;
377
+ const customerId = variables["payment.customerId"] || null;
391
378
  return {
392
- customerId: tracker.getCustomerId(),
379
+ customerId,
393
380
  isAuthorized: !!last4,
394
381
  loading: !!variables["payment.loading"],
395
382
  error: variables["payment.error"] || null,
396
383
  cardDetails: last4 ? { last4, brand, expMonth, expYear } : null
397
384
  };
398
- }, [variables, tracker]);
385
+ }, [variables]);
386
+ }
387
+ var DEVICE_KEYS = [
388
+ "os.name",
389
+ "os.timezone",
390
+ "device.type",
391
+ "device.isMobile",
392
+ "device.isTablet",
393
+ "device.screenWidth",
394
+ "device.screenHeight",
395
+ "device.viewportWidth",
396
+ "device.viewportHeight",
397
+ "device.colorDepth",
398
+ "device.pixelRatio",
399
+ "browser.name",
400
+ "browser.version",
401
+ "browser.userAgent",
402
+ "browser.cookieEnabled",
403
+ "browser.online",
404
+ "browser.language"
405
+ ];
406
+ function useDeviceInfo() {
407
+ const { variableStore } = useFunnelContext();
408
+ const subscribe = useCallback(
409
+ (cb) => variableStore.subscribe(cb, { keys: DEVICE_KEYS }),
410
+ [variableStore]
411
+ );
412
+ const getSnapshot = useCallback(
413
+ () => variableStore.getState(),
414
+ [variableStore]
415
+ );
416
+ const variables = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
417
+ return useMemo(() => ({
418
+ os: {
419
+ name: variables["os.name"] || "",
420
+ timezone: variables["os.timezone"] || ""
421
+ },
422
+ device: {
423
+ type: variables["device.type"] || "desktop",
424
+ isMobile: !!variables["device.isMobile"],
425
+ isTablet: !!variables["device.isTablet"],
426
+ screenWidth: variables["device.screenWidth"] || 0,
427
+ screenHeight: variables["device.screenHeight"] || 0,
428
+ viewportWidth: variables["device.viewportWidth"] || 0,
429
+ viewportHeight: variables["device.viewportHeight"] || 0,
430
+ colorDepth: variables["device.colorDepth"] || 24,
431
+ pixelRatio: variables["device.pixelRatio"] || 1
432
+ },
433
+ browser: {
434
+ name: variables["browser.name"] || "",
435
+ version: variables["browser.version"] || "",
436
+ userAgent: variables["browser.userAgent"] || "",
437
+ cookieEnabled: variables["browser.cookieEnabled"] !== false,
438
+ online: variables["browser.online"] !== false,
439
+ language: variables["browser.language"] || "en"
440
+ }
441
+ }), [variables]);
442
+ }
443
+ function useSafeArea() {
444
+ const [insets, setInsets] = useState({ top: 0, right: 0, bottom: 0, left: 0 });
445
+ useEffect(() => {
446
+ if (typeof window === "undefined") return;
447
+ const el = document.createElement("div");
448
+ el.style.cssText = [
449
+ "position:fixed",
450
+ "top:env(safe-area-inset-top,0px)",
451
+ "right:env(safe-area-inset-right,0px)",
452
+ "bottom:env(safe-area-inset-bottom,0px)",
453
+ "left:env(safe-area-inset-left,0px)",
454
+ "pointer-events:none",
455
+ "visibility:hidden",
456
+ "z-index:-1"
457
+ ].join(";");
458
+ document.body.appendChild(el);
459
+ function read() {
460
+ const style = getComputedStyle(el);
461
+ setInsets({
462
+ top: parseFloat(style.top) || 0,
463
+ right: parseFloat(style.right) || 0,
464
+ bottom: parseFloat(style.bottom) || 0,
465
+ left: parseFloat(style.left) || 0
466
+ });
467
+ }
468
+ read();
469
+ const observer = new ResizeObserver(read);
470
+ observer.observe(el);
471
+ window.addEventListener("resize", read);
472
+ return () => {
473
+ observer.disconnect();
474
+ window.removeEventListener("resize", read);
475
+ el.remove();
476
+ };
477
+ }, []);
478
+ return insets;
479
+ }
480
+ function useKeyboard() {
481
+ const [state, setState] = useState({ isOpen: false, height: 0 });
482
+ const timeoutRef = useRef();
483
+ useEffect(() => {
484
+ if (typeof window === "undefined") return;
485
+ if ("virtualKeyboard" in navigator) {
486
+ const vk = navigator.virtualKeyboard;
487
+ vk.overlaysContent = true;
488
+ const handler = () => {
489
+ const h = vk.boundingRect.height;
490
+ setState((prev) => {
491
+ if (prev.height === h && prev.isOpen === h > 0) return prev;
492
+ return { isOpen: h > 0, height: h };
493
+ });
494
+ };
495
+ vk.addEventListener("geometrychange", handler);
496
+ handler();
497
+ return () => vk.removeEventListener("geometrychange", handler);
498
+ }
499
+ const vv = window.visualViewport;
500
+ if (!vv) return;
501
+ let layoutHeight = window.innerHeight;
502
+ function compute() {
503
+ const kbHeight = Math.max(0, layoutHeight - vv.height);
504
+ const h = kbHeight > 40 ? Math.round(kbHeight) : 0;
505
+ setState((prev) => {
506
+ if (prev.height === h && prev.isOpen === h > 0) return prev;
507
+ return { isOpen: h > 0, height: h };
508
+ });
509
+ }
510
+ function onViewportResize() {
511
+ compute();
512
+ clearTimeout(timeoutRef.current);
513
+ timeoutRef.current = setTimeout(compute, 1e3);
514
+ }
515
+ function onWindowResize() {
516
+ layoutHeight = window.innerHeight;
517
+ compute();
518
+ }
519
+ vv.addEventListener("resize", onViewportResize);
520
+ window.addEventListener("resize", onWindowResize);
521
+ window.addEventListener("orientationchange", onWindowResize);
522
+ compute();
523
+ return () => {
524
+ clearTimeout(timeoutRef.current);
525
+ vv.removeEventListener("resize", onViewportResize);
526
+ window.removeEventListener("resize", onWindowResize);
527
+ window.removeEventListener("orientationchange", onWindowResize);
528
+ };
529
+ }, []);
530
+ return state;
531
+ }
532
+ var PAGE_KEYS = [
533
+ "page.currentId",
534
+ "page.currentIndex",
535
+ "page.current",
536
+ "page.total",
537
+ "page.progressPercentage",
538
+ "page.startedAt"
539
+ ];
540
+ function usePageData() {
541
+ const { variableStore } = useFunnelContext();
542
+ const subscribe = useCallback(
543
+ (cb) => variableStore.subscribe(cb, { keys: PAGE_KEYS }),
544
+ [variableStore]
545
+ );
546
+ const getSnapshot = useCallback(
547
+ () => variableStore.getState(),
548
+ [variableStore]
549
+ );
550
+ const variables = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
551
+ return useMemo(() => ({
552
+ currentId: variables["page.currentId"] || "",
553
+ currentIndex: variables["page.currentIndex"] || 0,
554
+ current: variables["page.current"] || 1,
555
+ total: variables["page.total"] || 0,
556
+ progressPercentage: variables["page.progressPercentage"] || 0,
557
+ startedAt: variables["page.startedAt"] || 0
558
+ }), [variables]);
399
559
  }
400
560
 
401
561
  // src/hooks/useFunnel.ts
@@ -415,242 +575,282 @@ function useFunnel() {
415
575
  payment: usePayment()
416
576
  };
417
577
  }
418
- function InnerPaymentForm({
419
- paymentMode,
420
- validateOnly,
421
- onSuccess,
422
- onError
423
- }) {
424
- const stripe = useStripe();
425
- const elements = useElements();
426
- const [error, setError] = useState(null);
427
- const { variableStore, campaignId, tracker, apiBaseUrl, products } = useFunnelContext();
428
- const handleSubmit = useCallback(async () => {
429
- if (!stripe || !elements) {
430
- const msg = "Stripe not loaded";
431
- setError(msg);
432
- onError?.(msg);
433
- return;
434
- }
435
- setError(null);
436
- variableStore.set("payment.loading", true);
437
- try {
438
- const confirmFn = paymentMode === "setup" ? stripe.confirmSetup : stripe.confirmPayment;
439
- const confirmResult = await confirmFn({
440
- elements,
441
- redirect: "if_required",
442
- confirmParams: { return_url: window.location.href }
443
- });
444
- if (confirmResult.error) {
445
- const msg = confirmResult.error.message || "Payment failed";
578
+ var API_BASE_URL = "https://api.appfunnel.net";
579
+ var InnerPaymentForm = forwardRef(
580
+ function InnerPaymentForm2({ paymentMode, validateOnly, onSuccess, onError, onReady, layout }, ref) {
581
+ const stripe = useStripe();
582
+ const elements = useElements();
583
+ const [error, setError] = useState(null);
584
+ const { variableStore, campaignId, tracker, products } = useFunnelContext();
585
+ const readyFired = useRef(false);
586
+ useEffect(() => {
587
+ if (stripe && elements && !readyFired.current) {
588
+ readyFired.current = true;
589
+ onReady?.();
590
+ }
591
+ }, [stripe, elements, onReady]);
592
+ const handleSubmit = useCallback(async () => {
593
+ if (!stripe || !elements) {
594
+ const msg = "Stripe not loaded";
446
595
  setError(msg);
447
- variableStore.set("payment.error", msg);
448
596
  onError?.(msg);
449
597
  return;
450
598
  }
451
- tracker.track("checkout.payment_added");
452
- if (validateOnly) {
453
- const piId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent : void 0;
454
- if (!piId?.id) {
455
- const msg = "PaymentIntent not found after confirmation";
599
+ setError(null);
600
+ variableStore.set("payment.loading", true);
601
+ try {
602
+ const confirmFn = paymentMode === "setup" ? stripe.confirmSetup : stripe.confirmPayment;
603
+ const confirmResult = await confirmFn({
604
+ elements,
605
+ redirect: "if_required",
606
+ confirmParams: { return_url: window.location.href }
607
+ });
608
+ if (confirmResult.error) {
609
+ const msg = confirmResult.error.message || "Payment failed";
456
610
  setError(msg);
457
611
  variableStore.set("payment.error", msg);
458
612
  onError?.(msg);
459
613
  return;
460
614
  }
461
- const response2 = await fetch(
462
- `${apiBaseUrl}/campaign/${campaignId}/stripe/validate-card`,
615
+ tracker.track("checkout.payment_added");
616
+ if (validateOnly) {
617
+ const piId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent : void 0;
618
+ if (!piId?.id) {
619
+ const msg = "PaymentIntent not found after confirmation";
620
+ setError(msg);
621
+ variableStore.set("payment.error", msg);
622
+ onError?.(msg);
623
+ return;
624
+ }
625
+ const response2 = await fetch(
626
+ `${API_BASE_URL}/campaign/${campaignId}/stripe/validate-card`,
627
+ {
628
+ method: "POST",
629
+ headers: { "Content-Type": "application/json" },
630
+ body: JSON.stringify({
631
+ campaignId,
632
+ sessionId: tracker.getSessionId(),
633
+ paymentIntentId: piId.id
634
+ })
635
+ }
636
+ );
637
+ const result2 = await response2.json();
638
+ if (!result2.success) {
639
+ const msg = result2.error || "Card validation failed";
640
+ setError(msg);
641
+ variableStore.set("payment.error", msg);
642
+ onError?.(msg);
643
+ return;
644
+ }
645
+ variableStore.setMany({
646
+ "card.last4": result2.card.last4,
647
+ "card.brand": result2.card.brand,
648
+ "card.expMonth": result2.card.expMonth,
649
+ "card.expYear": result2.card.expYear,
650
+ "card.funding": result2.card.funding,
651
+ "payment.error": ""
652
+ });
653
+ onSuccess?.();
654
+ return;
655
+ }
656
+ const paymentIntentId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent?.id : void 0;
657
+ const selectedId = variableStore.get("products.selectedProductId");
658
+ const product = products.find((p) => p.id === selectedId);
659
+ if (!product?.stripePriceId) {
660
+ const msg = "No product selected or missing Stripe price";
661
+ setError(msg);
662
+ variableStore.set("payment.error", msg);
663
+ onError?.(msg);
664
+ return;
665
+ }
666
+ const response = await fetch(
667
+ `${API_BASE_URL}/campaign/${campaignId}/stripe/purchase`,
463
668
  {
464
669
  method: "POST",
465
670
  headers: { "Content-Type": "application/json" },
466
671
  body: JSON.stringify({
467
672
  campaignId,
468
673
  sessionId: tracker.getSessionId(),
469
- paymentIntentId: piId.id
674
+ stripePriceId: product.stripePriceId,
675
+ trialPeriodDays: product.hasTrial ? product.trialDays : void 0,
676
+ onSessionPiId: paymentMode === "payment" ? paymentIntentId : void 0
470
677
  })
471
678
  }
472
679
  );
473
- const result2 = await response2.json();
474
- if (!result2.success) {
475
- const msg = result2.error || "Card validation failed";
680
+ const result = await response.json();
681
+ if (result.success) {
682
+ variableStore.set("payment.error", "");
683
+ if (result.eventId || result.eventIds?.firstPeriod) {
684
+ tracker.track("purchase.complete", {
685
+ amount: product.rawPrice ? product.rawPrice / 100 : 0,
686
+ currency: product.currencyCode || "USD"
687
+ });
688
+ }
689
+ onSuccess?.();
690
+ } else {
691
+ const msg = result.error || "Failed to process payment";
476
692
  setError(msg);
477
693
  variableStore.set("payment.error", msg);
478
694
  onError?.(msg);
479
- return;
480
695
  }
481
- variableStore.setMany({
482
- "card.last4": result2.card.last4,
483
- "card.brand": result2.card.brand,
484
- "card.expMonth": result2.card.expMonth,
485
- "card.expYear": result2.card.expYear,
486
- "card.funding": result2.card.funding,
487
- "payment.error": ""
488
- });
489
- onSuccess?.();
490
- return;
491
- }
492
- const paymentIntentId = "paymentIntent" in confirmResult ? confirmResult.paymentIntent?.id : void 0;
493
- const selectedId = variableStore.get("products.selectedProductId");
494
- const product = products.find((p) => p.id === selectedId);
495
- if (!product?.stripePriceId) {
496
- const msg = "No product selected or missing Stripe price";
696
+ } catch (err) {
697
+ const msg = err instanceof Error ? err.message : "An error occurred";
497
698
  setError(msg);
498
699
  variableStore.set("payment.error", msg);
499
700
  onError?.(msg);
500
- return;
701
+ } finally {
702
+ variableStore.set("payment.loading", false);
501
703
  }
502
- const variables = variableStore.getState();
503
- await tracker.updateUserData(variables);
504
- const response = await fetch(
505
- `${apiBaseUrl}/campaign/${campaignId}/stripe/purchase`,
506
- {
507
- method: "POST",
508
- headers: { "Content-Type": "application/json" },
509
- body: JSON.stringify({
510
- campaignId,
511
- sessionId: tracker.getSessionId(),
512
- stripePriceId: product.stripePriceId,
513
- trialPeriodDays: product.hasTrial ? product.trialDays : void 0,
514
- onSessionPiId: paymentMode === "payment" ? paymentIntentId : void 0
515
- })
516
- }
517
- );
518
- const result = await response.json();
519
- if (result.success) {
520
- variableStore.set("payment.error", "");
521
- if (result.eventId || result.eventIds?.firstPeriod) {
522
- tracker.track("purchase.complete", {
523
- amount: product.rawPrice ? product.rawPrice / 100 : 0,
524
- currency: product.currencyCode || "USD"
704
+ }, [stripe, elements, paymentMode, validateOnly, variableStore, campaignId, tracker, products, onSuccess, onError]);
705
+ useImperativeHandle(ref, () => ({ submit: handleSubmit }), [handleSubmit]);
706
+ return /* @__PURE__ */ jsxs("div", { children: [
707
+ /* @__PURE__ */ jsx(PaymentElement, { options: { layout: layout || "tabs" } }),
708
+ error && /* @__PURE__ */ jsx("div", { style: { color: "#ef4444", fontSize: "14px", marginTop: "12px" }, children: error })
709
+ ] });
710
+ }
711
+ );
712
+ var StripePaymentForm = forwardRef(
713
+ function StripePaymentForm2({
714
+ productId,
715
+ mode = "checkout",
716
+ variant = "elements",
717
+ onSuccess,
718
+ onError,
719
+ onReady,
720
+ className,
721
+ appearance,
722
+ layout
723
+ }, ref) {
724
+ const { campaignId, tracker, variableStore, products } = useFunnelContext();
725
+ const [email] = useVariable("user.email");
726
+ const validateOnly = mode === "validate-only";
727
+ const product = useMemo(() => {
728
+ if (productId) return products.find((p) => p.id === productId) || null;
729
+ const selectedId = variableStore.get("products.selectedProductId");
730
+ return products.find((p) => p.id === selectedId) || null;
731
+ }, [productId, products, variableStore]);
732
+ const paymentMode = useMemo(() => {
733
+ if (validateOnly) return "payment";
734
+ if (product?.hasTrial && !product.paidTrial) return "setup";
735
+ return "payment";
736
+ }, [product, validateOnly]);
737
+ const [stripePromise, setStripePromise] = useState(null);
738
+ const [clientSecret, setClientSecret] = useState(null);
739
+ const [error, setError] = useState(null);
740
+ const [isLoading, setIsLoading] = useState(false);
741
+ const hasInitialized = useRef(false);
742
+ useEffect(() => {
743
+ if (!email || !campaignId || hasInitialized.current) return;
744
+ if (!product?.storePriceId) return;
745
+ hasInitialized.current = true;
746
+ setIsLoading(true);
747
+ variableStore.set("payment.loading", true);
748
+ const createIntent = async () => {
749
+ try {
750
+ if (variant === "embedded") {
751
+ const response = await fetch(
752
+ `${API_BASE_URL}/campaign/${campaignId}/stripe/checkout-session`,
753
+ {
754
+ method: "POST",
755
+ headers: { "Content-Type": "application/json" },
756
+ body: JSON.stringify({
757
+ campaignId,
758
+ sessionId: tracker.getSessionId(),
759
+ customerEmail: email,
760
+ priceId: product.storePriceId,
761
+ trialPeriodDays: product.hasTrial ? product.trialDays : void 0
762
+ })
763
+ }
764
+ );
765
+ const result = await response.json();
766
+ if (!result.success) throw new Error(result.error || "Failed to create checkout session");
767
+ setStripePromise(loadStripe(result.publishableKey));
768
+ setClientSecret(result.clientSecret);
769
+ } else {
770
+ const endpoint = paymentMode === "setup" ? `/campaign/${campaignId}/stripe/setup-intent` : `/campaign/${campaignId}/stripe/payment-intent`;
771
+ const body = {
772
+ campaignId,
773
+ sessionId: tracker.getSessionId(),
774
+ customerEmail: email,
775
+ priceId: product.storePriceId
776
+ };
777
+ if (validateOnly) body.validateOnly = true;
778
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
779
+ method: "POST",
780
+ headers: { "Content-Type": "application/json" },
781
+ body: JSON.stringify(body)
782
+ });
783
+ const result = await response.json();
784
+ if (!result.success) throw new Error(result.error || "Failed to create intent");
785
+ setStripePromise(loadStripe(result.publishableKey));
786
+ setClientSecret(result.clientSecret);
787
+ variableStore.set("user.stripeCustomerId", result.customerId);
788
+ }
789
+ tracker.track("checkout.start", {
790
+ productId: product.id,
791
+ priceId: product.stripePriceId || product.storePriceId,
792
+ productName: product.displayName
525
793
  });
794
+ } catch (err) {
795
+ const msg = err instanceof Error ? err.message : "Failed to initialize payment";
796
+ setError(msg);
797
+ variableStore.set("payment.error", msg);
798
+ } finally {
799
+ setIsLoading(false);
800
+ variableStore.set("payment.loading", false);
526
801
  }
527
- onSuccess?.();
528
- } else {
529
- const msg = result.error || "Failed to process payment";
530
- setError(msg);
531
- variableStore.set("payment.error", msg);
532
- onError?.(msg);
533
- }
534
- } catch (err) {
535
- const msg = err instanceof Error ? err.message : "An error occurred";
536
- setError(msg);
537
- variableStore.set("payment.error", msg);
538
- onError?.(msg);
539
- } finally {
540
- variableStore.set("payment.loading", false);
802
+ };
803
+ createIntent();
804
+ }, [email, campaignId, product, paymentMode, validateOnly, variant, tracker, variableStore]);
805
+ if (isLoading) {
806
+ return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Loading payment form..." });
541
807
  }
542
- }, [stripe, elements, paymentMode, validateOnly, variableStore, campaignId, tracker, apiBaseUrl, products, onSuccess, onError]);
543
- useEffect(() => {
544
- if (typeof window !== "undefined") {
545
- window.__paymentElementSubmit = handleSubmit;
808
+ if (!email) {
809
+ return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: "Email is required to initialize payment" });
546
810
  }
547
- return () => {
548
- if (typeof window !== "undefined") {
549
- delete window.__paymentElementSubmit;
550
- }
551
- };
552
- }, [handleSubmit]);
553
- return /* @__PURE__ */ jsxs("div", { children: [
554
- /* @__PURE__ */ jsx(PaymentElement, { options: { layout: "tabs" } }),
555
- error && /* @__PURE__ */ jsx("div", { style: { color: "#ef4444", fontSize: "14px", marginTop: "12px" }, children: error })
556
- ] });
557
- }
558
- function PaymentForm({
559
- productId,
560
- mode = "checkout",
561
- onSuccess,
562
- onError,
563
- className,
564
- appearance
565
- }) {
566
- const { campaignId, tracker, variableStore, apiBaseUrl, products } = useFunnelContext();
567
- const [email] = useVariable("user.email");
568
- const validateOnly = mode === "validate-only";
569
- const product = useMemo(() => {
570
- if (productId) return products.find((p) => p.id === productId) || null;
571
- const selectedId = variableStore.get("products.selectedProductId");
572
- return products.find((p) => p.id === selectedId) || null;
573
- }, [productId, products, variableStore]);
574
- const paymentMode = useMemo(() => {
575
- if (validateOnly) return "payment";
576
- if (product?.hasTrial && !product.paidTrial) return "setup";
577
- return "payment";
578
- }, [product, validateOnly]);
579
- const [stripePromise, setStripePromise] = useState(null);
580
- const [clientSecret, setClientSecret] = useState(null);
581
- const [error, setError] = useState(null);
582
- const [isLoading, setIsLoading] = useState(false);
583
- const hasInitialized = useRef(false);
584
- useEffect(() => {
585
- if (!email || !campaignId || hasInitialized.current) return;
586
- if (!product?.storePriceId) return;
587
- hasInitialized.current = true;
588
- setIsLoading(true);
589
- variableStore.set("payment.loading", true);
590
- const createIntent = async () => {
591
- try {
592
- const endpoint = paymentMode === "setup" ? `/campaign/${campaignId}/stripe/setup-intent` : `/campaign/${campaignId}/stripe/payment-intent`;
593
- const body = {
594
- campaignId,
595
- sessionId: tracker.getSessionId(),
596
- customerEmail: email,
597
- priceId: product.storePriceId
598
- };
599
- if (validateOnly) body.validateOnly = true;
600
- const response = await fetch(`${apiBaseUrl}${endpoint}`, {
601
- method: "POST",
602
- headers: { "Content-Type": "application/json" },
603
- body: JSON.stringify(body)
604
- });
605
- const result = await response.json();
606
- if (!result.success) {
607
- throw new Error(result.error || "Failed to create intent");
811
+ if (error) {
812
+ return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: error });
813
+ }
814
+ if (!stripePromise || !clientSecret) {
815
+ return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Initializing payment..." });
816
+ }
817
+ if (variant === "embedded") {
818
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(
819
+ EmbeddedCheckoutProvider,
820
+ {
821
+ stripe: stripePromise,
822
+ options: {
823
+ clientSecret,
824
+ onComplete: () => {
825
+ tracker.track("purchase.complete", {
826
+ amount: product?.rawPrice ? product.rawPrice / 100 : 0,
827
+ currency: product?.currencyCode || "USD"
828
+ });
829
+ onSuccess?.();
830
+ }
831
+ },
832
+ children: /* @__PURE__ */ jsx(EmbeddedCheckout, {})
608
833
  }
609
- setStripePromise(loadStripe(result.publishableKey));
610
- setClientSecret(result.clientSecret);
611
- variableStore.set("user.stripeCustomerId", result.customerId);
612
- tracker.track("checkout.start", {
613
- productId: product.id,
614
- priceId: product.stripePriceId || product.storePriceId,
615
- productName: product.displayName
616
- });
617
- } catch (err) {
618
- const msg = err instanceof Error ? err.message : "Failed to initialize payment";
619
- setError(msg);
620
- variableStore.set("payment.error", msg);
621
- } finally {
622
- setIsLoading(false);
623
- variableStore.set("payment.loading", false);
624
- }
834
+ ) });
835
+ }
836
+ const defaultAppearance = {
837
+ theme: "stripe",
838
+ variables: { colorPrimary: "#3b82f6", borderRadius: "8px" }
625
839
  };
626
- createIntent();
627
- }, [email, campaignId, product, paymentMode, validateOnly, tracker, variableStore, apiBaseUrl]);
628
- if (isLoading) {
629
- return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Loading payment form..." });
630
- }
631
- if (!email) {
632
- return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: "Email is required to initialize payment" });
633
- }
634
- if (error) {
635
- return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#ef4444", fontSize: "14px" }, children: error });
636
- }
637
- if (!stripePromise || !clientSecret) {
638
- return /* @__PURE__ */ jsx("div", { className, style: { padding: "20px", textAlign: "center", color: "#6b7280" }, children: "Initializing payment..." });
840
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options: { clientSecret, appearance: appearance || defaultAppearance }, children: /* @__PURE__ */ jsx(
841
+ InnerPaymentForm,
842
+ {
843
+ ref,
844
+ paymentMode,
845
+ validateOnly,
846
+ onSuccess,
847
+ onError,
848
+ onReady,
849
+ layout
850
+ }
851
+ ) }) });
639
852
  }
640
- const defaultAppearance = {
641
- theme: "stripe",
642
- variables: { colorPrimary: "#3b82f6", borderRadius: "8px" }
643
- };
644
- return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options: { clientSecret, appearance: appearance || defaultAppearance }, children: /* @__PURE__ */ jsx(
645
- InnerPaymentForm,
646
- {
647
- paymentMode,
648
- validateOnly,
649
- onSuccess,
650
- onError
651
- }
652
- ) }) });
653
- }
853
+ );
654
854
  function PaddleCheckout({
655
855
  productId,
656
856
  mode = "overlay",
@@ -715,6 +915,6 @@ function PaddleCheckout({
715
915
  return null;
716
916
  }
717
917
 
718
- export { PaddleCheckout, PaymentForm, defineConfig, definePage, registerIntegration, useData, useFunnel, useLocale, useNavigation, usePayment, useProducts, useQueryParam, useQueryParams, useResponse, useResponses, useTracking, useTranslation, useUser, useUserProperty, useVariable, useVariables };
918
+ export { PaddleCheckout, StripePaymentForm, defineConfig, definePage, useData, useDateOfBirth, useDeviceInfo, useFunnel, useKeyboard, useLocale, usePageData, usePayment, useProducts, useQueryParam, useQueryParams, useSafeArea, useTracking, useTranslation, useUser, useUserProperty, useVariable, useVariables };
719
919
  //# sourceMappingURL=index.js.map
720
920
  //# sourceMappingURL=index.js.map