@appfunnel-dev/sdk 0.6.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,5 +1,7 @@
1
- import { useFunnelContext } from './chunk-E6KSJ5UI.js';
2
- export { FunnelProvider, registerIntegration } from './chunk-E6KSJ5UI.js';
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';
3
5
  import { forwardRef, useState, useRef, useEffect, useCallback, useImperativeHandle, useMemo, useSyncExternalStore } from 'react';
4
6
  import { loadStripe } from '@stripe/stripe-js';
5
7
  import { useStripe, useElements, PaymentElement, EmbeddedCheckoutProvider, EmbeddedCheckout, Elements } from '@stripe/react-stripe-js';
@@ -41,6 +43,102 @@ function useVariables() {
41
43
  );
42
44
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
43
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
44
142
  function useUser() {
45
143
  const { variableStore } = useFunnelContext();
46
144
  const subscribe = useCallback(
@@ -66,7 +164,7 @@ function useUser() {
66
164
  variableStore.set("user.name", name);
67
165
  },
68
166
  setDateOfBirth(dateOfBirth) {
69
- variableStore.set("user.dateOfBirth", dateOfBirth);
167
+ variableStore.set("user.dateOfBirth", toISODate(dateOfBirth));
70
168
  }
71
169
  }),
72
170
  [variables, variableStore]
@@ -90,45 +188,24 @@ function useUserProperty(field) {
90
188
  );
91
189
  return [value, setValue];
92
190
  }
93
- function useResponse(key) {
191
+ function useDateOfBirth(format = "MM/DD/YYYY") {
94
192
  const { variableStore } = useFunnelContext();
95
- const prefixedKey = `answers.${key}`;
193
+ const key = "user.dateOfBirth";
96
194
  const subscribe = useCallback(
97
- (cb) => variableStore.subscribe(cb, { keys: [prefixedKey] }),
98
- [variableStore, prefixedKey]
195
+ (cb) => variableStore.subscribe(cb, { keys: [key] }),
196
+ [variableStore]
99
197
  );
100
198
  const getSnapshot = useCallback(
101
- () => variableStore.get(prefixedKey),
102
- [variableStore, prefixedKey]
199
+ () => variableStore.get(key) || "",
200
+ [variableStore]
103
201
  );
104
202
  const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
105
203
  const setValue = useCallback(
106
- (v) => variableStore.set(prefixedKey, v),
107
- [variableStore, prefixedKey]
204
+ (v) => variableStore.set(key, toISODateWithFormat(v, format)),
205
+ [variableStore, format]
108
206
  );
109
207
  return [value, setValue];
110
208
  }
111
- function useResponses() {
112
- const { variableStore } = useFunnelContext();
113
- const subscribe = useCallback(
114
- (cb) => variableStore.subscribe(cb, { prefix: "answers." }),
115
- [variableStore]
116
- );
117
- const getSnapshot = useCallback(
118
- () => variableStore.getState(),
119
- [variableStore]
120
- );
121
- const variables = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
122
- return useMemo(() => {
123
- const result = {};
124
- for (const [key, value] of Object.entries(variables)) {
125
- if (key.startsWith("answers.")) {
126
- result[key.slice(8)] = value;
127
- }
128
- }
129
- return result;
130
- }, [variables]);
131
- }
132
209
  function useQueryParams() {
133
210
  const { variableStore } = useFunnelContext();
134
211
  const subscribe = useCallback(
@@ -239,64 +316,6 @@ function useTranslation() {
239
316
  const availableLocales = i18n.getAvailableLocales();
240
317
  return { t, locale, setLocale, availableLocales };
241
318
  }
242
- function useNavigation() {
243
- const { router, variableStore, tracker } = useFunnelContext();
244
- useSyncExternalStore(
245
- useCallback((cb) => router.subscribe(cb), [router]),
246
- useCallback(() => router.getSnapshot(), [router]),
247
- useCallback(() => router.getSnapshot(), [router])
248
- );
249
- const afterNavigate = useCallback((key) => {
250
- const page = router.getCurrentPage();
251
- if (page) {
252
- tracker.track("page.view", {
253
- pageId: page.key,
254
- pageKey: page.key,
255
- pageName: page.name
256
- });
257
- tracker.startPageTracking(page.key);
258
- }
259
- variableStore.setMany({
260
- "page.currentId": key,
261
- "page.currentIndex": router.getPageHistory().length,
262
- "page.current": router.getPageHistory().length + 1,
263
- "page.startedAt": Date.now()
264
- });
265
- }, [router, tracker, variableStore]);
266
- const goToNextPage = useCallback(() => {
267
- const previousPage = router.getCurrentPage();
268
- if (previousPage) {
269
- tracker.stopPageTracking();
270
- }
271
- const variables = variableStore.getState();
272
- const nextKey = router.goToNextPage(variables);
273
- if (nextKey) {
274
- afterNavigate(nextKey);
275
- }
276
- }, [router, tracker, variableStore, afterNavigate]);
277
- const goBack = useCallback(() => {
278
- tracker.stopPageTracking();
279
- const prevKey = router.goBack();
280
- if (prevKey) {
281
- afterNavigate(prevKey);
282
- }
283
- }, [router, tracker, afterNavigate]);
284
- const goToPage = useCallback((pageKey) => {
285
- tracker.stopPageTracking();
286
- const key = router.goToPage(pageKey);
287
- if (key) {
288
- afterNavigate(key);
289
- }
290
- }, [router, tracker, afterNavigate]);
291
- return {
292
- goToNextPage,
293
- goBack,
294
- goToPage,
295
- currentPage: router.getCurrentPage(),
296
- pageHistory: router.getPageHistory(),
297
- progress: router.getProgress()
298
- };
299
- }
300
319
  function useProducts() {
301
320
  const { products, variableStore, selectProduct: ctxSelect } = useFunnelContext();
302
321
  const subscribe = useCallback(
@@ -421,6 +440,95 @@ function useDeviceInfo() {
421
440
  }
422
441
  }), [variables]);
423
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
+ }
424
532
  var PAGE_KEYS = [
425
533
  "page.currentId",
426
534
  "page.currentIndex",
@@ -807,6 +915,6 @@ function PaddleCheckout({
807
915
  return null;
808
916
  }
809
917
 
810
- export { PaddleCheckout, StripePaymentForm, defineConfig, definePage, useData, useDeviceInfo, useFunnel, useLocale, useNavigation, usePageData, 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 };
811
919
  //# sourceMappingURL=index.js.map
812
920
  //# sourceMappingURL=index.js.map