@appfunnel-dev/sdk 2.0.0-canary.4 → 2.0.0-canary.7
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/README.md +1 -1
- package/dist/{checkout-Dz8cGkB_.d.cts → checkout-CMEdxpgo.d.cts} +13 -13
- package/dist/{checkout-7Dy6IedP.d.ts → checkout-D1NUPv4p.d.ts} +13 -13
- package/dist/{chunk-AKO6XKXP.js → chunk-7YQLPPPG.js} +66 -91
- package/dist/chunk-7YQLPPPG.js.map +1 -0
- package/dist/{chunk-JSRKA375.cjs → chunk-BPOAWI4G.cjs} +68 -94
- package/dist/chunk-BPOAWI4G.cjs.map +1 -0
- package/dist/{chunk-M6U3FNRW.js → chunk-CFFMZYPE.js} +3 -3
- package/dist/{chunk-M6U3FNRW.js.map → chunk-CFFMZYPE.js.map} +1 -1
- package/dist/chunk-EMMSS5I5.cjs +37 -0
- package/dist/chunk-EMMSS5I5.cjs.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/{chunk-LJYLGLFS.cjs → chunk-QMAZGLGV.cjs} +2 -35
- package/dist/chunk-QMAZGLGV.cjs.map +1 -0
- package/dist/{chunk-YY375F2B.js → chunk-TNLRQPVY.js} +3 -33
- package/dist/chunk-TNLRQPVY.js.map +1 -0
- package/dist/{chunk-CY4VBSMX.cjs → chunk-VAOR77NW.cjs} +7 -7
- package/dist/{chunk-CY4VBSMX.cjs.map → chunk-VAOR77NW.cjs.map} +1 -1
- package/dist/chunk-WYUDL4FI.cjs +8 -0
- package/dist/chunk-WYUDL4FI.cjs.map +1 -0
- package/dist/chunk-ZZJG4EYL.js +6 -0
- package/dist/chunk-ZZJG4EYL.js.map +1 -0
- package/dist/driver-paddle.cjs +23 -22
- package/dist/driver-paddle.cjs.map +1 -1
- package/dist/driver-paddle.d.cts +1 -1
- package/dist/driver-paddle.d.ts +1 -1
- package/dist/driver-paddle.js +3 -2
- package/dist/driver-paddle.js.map +1 -1
- package/dist/driver-stripe.cjs +277 -224
- package/dist/driver-stripe.cjs.map +1 -1
- package/dist/driver-stripe.d.cts +18 -2
- package/dist/driver-stripe.d.ts +18 -2
- package/dist/driver-stripe.js +244 -192
- package/dist/driver-stripe.js.map +1 -1
- package/dist/index.cjs +750 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -90
- package/dist/index.d.ts +151 -90
- package/dist/index.js +702 -98
- package/dist/index.js.map +1 -1
- package/dist/{manifest-Cr2y1op6.d.cts → manifest-CMe8yVkr.d.cts} +131 -88
- package/dist/{manifest-Cr2y1op6.d.ts → manifest-CMe8yVkr.d.ts} +131 -88
- package/dist/manifest-entry.cjs +62 -47
- package/dist/manifest-entry.cjs.map +1 -1
- package/dist/manifest-entry.d.cts +15 -15
- package/dist/manifest-entry.d.ts +15 -15
- package/dist/manifest-entry.js +25 -14
- package/dist/manifest-entry.js.map +1 -1
- package/dist/protocol.cjs +13 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +182 -0
- package/dist/protocol.d.ts +182 -0
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +14 -2
- package/dist/chunk-AKO6XKXP.js.map +0 -1
- package/dist/chunk-JSRKA375.cjs.map +0 -1
- package/dist/chunk-LJYLGLFS.cjs.map +0 -1
- package/dist/chunk-YY375F2B.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { isRtl,
|
|
2
|
-
export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney,
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { isRtl, formatOffering, bucketingSeed, resolveExperiments, entryGuard, evaluateGate, expectedPathLength, nextPage, outgoingKeys, buildCatalog, resolveLocale } from './chunk-7YQLPPPG.js';
|
|
2
|
+
export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney, formatOffering, hashToUnit, isRtl, isVariantKey, nextPage, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, resolveExperiments, resolveLocale, resolveOffering, resolveRoute } from './chunk-7YQLPPPG.js';
|
|
3
|
+
import { AF_PROTOCOL } from './chunk-ZZJG4EYL.js';
|
|
4
|
+
import { checkoutError, isInlineSurface, PROVIDER_PROFILES } from './chunk-TNLRQPVY.js';
|
|
5
|
+
export { INLINE_SURFACES, PROVIDER_PROFILES, checkoutError, isInlineSurface, isMerchantOfRecord, isOrchestrator, surfacesFor, validateCheckout, validateUpsell } from './chunk-TNLRQPVY.js';
|
|
6
|
+
import './chunk-G3PMV62Z.js';
|
|
5
7
|
import React, { createContext, createElement, useCallback, useContext, useMemo, lazy, useRef, useState, useEffect, useTransition, Suspense, Fragment, isValidElement, cloneElement, useReducer, useSyncExternalStore } from 'react';
|
|
6
8
|
import ReactDOM, { createPortal } from 'react-dom';
|
|
7
9
|
import { jsx } from 'react/jsx-runtime';
|
|
@@ -380,7 +382,40 @@ function createBus(accessors, now2 = () => Date.now()) {
|
|
|
380
382
|
}
|
|
381
383
|
}
|
|
382
384
|
};
|
|
383
|
-
|
|
385
|
+
let nav = null;
|
|
386
|
+
let warnedNoNav = false;
|
|
387
|
+
const currentNav = () => {
|
|
388
|
+
if (!nav && !warnedNoNav) {
|
|
389
|
+
warnedNoNav = true;
|
|
390
|
+
console.warn(
|
|
391
|
+
"[appfunnel] window.appfunnel navigation called before the funnel flow mounted \u2014 ignored"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return nav;
|
|
395
|
+
};
|
|
396
|
+
const registerNavigation = (n) => {
|
|
397
|
+
nav = n;
|
|
398
|
+
warnedNoNav = false;
|
|
399
|
+
return () => {
|
|
400
|
+
if (nav === n) nav = null;
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const goTo = (pageKey) => currentNav()?.goTo(pageKey) ?? false;
|
|
404
|
+
const goNext = () => currentNav()?.goNext();
|
|
405
|
+
const goPrevious = () => currentNav()?.goPrevious();
|
|
406
|
+
const navigation = () => nav?.snapshot() ?? null;
|
|
407
|
+
return {
|
|
408
|
+
on,
|
|
409
|
+
off,
|
|
410
|
+
events,
|
|
411
|
+
emit,
|
|
412
|
+
registerNavigation,
|
|
413
|
+
goTo,
|
|
414
|
+
goNext,
|
|
415
|
+
goPrevious,
|
|
416
|
+
navigation,
|
|
417
|
+
...accessors
|
|
418
|
+
};
|
|
384
419
|
}
|
|
385
420
|
function withBus(tracker, bus) {
|
|
386
421
|
return {
|
|
@@ -402,7 +437,19 @@ function withBus(tracker, bus) {
|
|
|
402
437
|
function attachBus(bus) {
|
|
403
438
|
if (typeof window === "undefined") return () => {
|
|
404
439
|
};
|
|
405
|
-
const {
|
|
440
|
+
const {
|
|
441
|
+
on,
|
|
442
|
+
off,
|
|
443
|
+
getVariable,
|
|
444
|
+
getVariables,
|
|
445
|
+
getCurrentPageId,
|
|
446
|
+
getCustomerId,
|
|
447
|
+
getVisitorId,
|
|
448
|
+
goTo,
|
|
449
|
+
goNext,
|
|
450
|
+
goPrevious,
|
|
451
|
+
navigation
|
|
452
|
+
} = bus;
|
|
406
453
|
const api = {
|
|
407
454
|
on,
|
|
408
455
|
off,
|
|
@@ -411,6 +458,11 @@ function attachBus(bus) {
|
|
|
411
458
|
getCurrentPageId,
|
|
412
459
|
getCustomerId,
|
|
413
460
|
getVisitorId,
|
|
461
|
+
// Flow control (bound closures over the bus's live nav registration).
|
|
462
|
+
goTo,
|
|
463
|
+
goNext,
|
|
464
|
+
goPrevious,
|
|
465
|
+
navigation,
|
|
414
466
|
get events() {
|
|
415
467
|
return bus.events;
|
|
416
468
|
}
|
|
@@ -538,20 +590,119 @@ function useCatalog() {
|
|
|
538
590
|
return useContext(CatalogContext) ?? EMPTY;
|
|
539
591
|
}
|
|
540
592
|
var EMPTY = /* @__PURE__ */ new Map();
|
|
541
|
-
function
|
|
593
|
+
function useOffering(id) {
|
|
542
594
|
const catalog = useCatalog();
|
|
543
595
|
const locale = useActiveLocale();
|
|
544
596
|
const resolved = id ? catalog.get(id) : void 0;
|
|
545
|
-
return useMemo(
|
|
597
|
+
return useMemo(
|
|
598
|
+
() => resolved ? formatOffering(resolved, locale) : void 0,
|
|
599
|
+
[resolved, locale]
|
|
600
|
+
);
|
|
546
601
|
}
|
|
547
|
-
function
|
|
602
|
+
function useOfferings() {
|
|
548
603
|
const catalog = useCatalog();
|
|
549
604
|
const locale = useActiveLocale();
|
|
550
605
|
return useMemo(
|
|
551
|
-
() => Array.from(catalog.values()).map((r) =>
|
|
606
|
+
() => Array.from(catalog.values()).map((r) => formatOffering(r, locale)),
|
|
552
607
|
[catalog, locale]
|
|
553
608
|
);
|
|
554
609
|
}
|
|
610
|
+
|
|
611
|
+
// src/flow/screenshot.ts
|
|
612
|
+
var MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
613
|
+
var ScreenshotError = class extends Error {
|
|
614
|
+
constructor(code, message) {
|
|
615
|
+
super(message);
|
|
616
|
+
this.code = code;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
function resolveTarget(t) {
|
|
620
|
+
if (!t || t.kind === "page") return document.getElementById("root") ?? document.body;
|
|
621
|
+
if (t.kind === "viewport") return document.body;
|
|
622
|
+
return document.querySelector(t.selector);
|
|
623
|
+
}
|
|
624
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
625
|
+
function hideAll(selectors) {
|
|
626
|
+
const touched = [];
|
|
627
|
+
for (const sel of selectors) {
|
|
628
|
+
document.querySelectorAll(sel).forEach((el) => {
|
|
629
|
+
touched.push([el, el.style.visibility]);
|
|
630
|
+
el.style.visibility = "hidden";
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return () => touched.forEach(([el, v]) => el.style.visibility = v);
|
|
634
|
+
}
|
|
635
|
+
async function scrollThrough(el) {
|
|
636
|
+
const prev = el.scrollTop;
|
|
637
|
+
const step = el.clientHeight || window.innerHeight;
|
|
638
|
+
for (let y = 0; y < el.scrollHeight; y += step) {
|
|
639
|
+
el.scrollTop = y;
|
|
640
|
+
await delay(16);
|
|
641
|
+
}
|
|
642
|
+
el.scrollTop = prev;
|
|
643
|
+
await delay(16);
|
|
644
|
+
}
|
|
645
|
+
function blobToDataURL(blob) {
|
|
646
|
+
return new Promise((resolve, reject) => {
|
|
647
|
+
const fr = new FileReader();
|
|
648
|
+
fr.onload = () => resolve(String(fr.result));
|
|
649
|
+
fr.onerror = () => reject(fr.error);
|
|
650
|
+
fr.readAsDataURL(blob);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async function captureScreenshot(params = {}) {
|
|
654
|
+
const el = resolveTarget(params.target);
|
|
655
|
+
if (!el) throw new ScreenshotError("bad_params", "screenshot target not found");
|
|
656
|
+
const restore = params.hideSelectors?.length ? hideAll(params.hideSelectors) : null;
|
|
657
|
+
const scale = Math.min(params.scale ?? 2, params.maxScale ?? 3);
|
|
658
|
+
const format = params.format ?? "png";
|
|
659
|
+
const timeout = params.timeout ?? 1e4;
|
|
660
|
+
try {
|
|
661
|
+
if (params.waitForFonts !== false && document.fonts?.ready) {
|
|
662
|
+
await Promise.race([document.fonts.ready, delay(timeout)]);
|
|
663
|
+
}
|
|
664
|
+
if (params.triggerLazyLoad) await scrollThrough(el);
|
|
665
|
+
if (params.waitForImages !== false) {
|
|
666
|
+
await Promise.all(
|
|
667
|
+
Array.from(el.querySelectorAll("img")).map(
|
|
668
|
+
(img) => typeof img.decode === "function" ? img.decode().catch(() => {
|
|
669
|
+
}) : Promise.resolve()
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const { domToBlob } = await import('modern-screenshot');
|
|
674
|
+
let blob;
|
|
675
|
+
try {
|
|
676
|
+
blob = await domToBlob(el, {
|
|
677
|
+
type: MIME[format],
|
|
678
|
+
quality: params.quality ?? 0.92,
|
|
679
|
+
scale,
|
|
680
|
+
width: params.width,
|
|
681
|
+
height: params.height,
|
|
682
|
+
// `null` = transparent (png/webp); only fall back to white when the caller omitted it. `??`
|
|
683
|
+
// would wrongly collapse an explicit `null` to white.
|
|
684
|
+
backgroundColor: params.backgroundColor === void 0 ? "#ffffff" : params.backgroundColor,
|
|
685
|
+
timeout
|
|
686
|
+
});
|
|
687
|
+
} catch (e) {
|
|
688
|
+
throw new ScreenshotError("failed", `capture failed (an image/font may be served without CORS '*'): ${String(e?.message || e)}`);
|
|
689
|
+
}
|
|
690
|
+
const result = {
|
|
691
|
+
mime: MIME[format],
|
|
692
|
+
width: Math.round((params.width ?? el.clientWidth) * scale),
|
|
693
|
+
height: Math.round((params.height ?? el.scrollHeight) * scale),
|
|
694
|
+
scale,
|
|
695
|
+
bytesApprox: blob.size
|
|
696
|
+
};
|
|
697
|
+
if ((params.encoding ?? "blob") === "dataURL") result.dataURL = await blobToDataURL(blob);
|
|
698
|
+
else result.blob = blob;
|
|
699
|
+
return result;
|
|
700
|
+
} finally {
|
|
701
|
+
restore?.();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/flow/flow.tsx
|
|
555
706
|
var NavContext = createContext(null);
|
|
556
707
|
var ExperimentContext = createContext({});
|
|
557
708
|
function useExperiment(id) {
|
|
@@ -604,6 +755,7 @@ function usePageExitTimer(tracker, currentKey) {
|
|
|
604
755
|
function FunnelView({
|
|
605
756
|
pages,
|
|
606
757
|
initialKey,
|
|
758
|
+
trustInitialKey = false,
|
|
607
759
|
layout,
|
|
608
760
|
fallback,
|
|
609
761
|
prefetch: prefetchMode = "auto",
|
|
@@ -643,14 +795,19 @@ function FunnelView({
|
|
|
643
795
|
[experiments, pageByKey]
|
|
644
796
|
);
|
|
645
797
|
const flow = useMemo(
|
|
646
|
-
() => flowPages.map((p) =>
|
|
647
|
-
|
|
798
|
+
() => flowPages.map((p) => {
|
|
799
|
+
const servedVariant = experiments.render[p.key];
|
|
800
|
+
const variantNext = servedVariant ? pageByKey.get(servedVariant)?.meta?.next : void 0;
|
|
801
|
+
return variantNext ? { key: p.key, meta: { ...p.meta ?? {}, next: variantNext } } : { key: p.key, meta: p.meta };
|
|
802
|
+
}),
|
|
803
|
+
[flowPages, experiments, pageByKey]
|
|
648
804
|
);
|
|
649
805
|
const [history, setHistory] = useState(() => {
|
|
650
806
|
const start = flowPages[0]?.key;
|
|
651
807
|
if (!initialKey || initialKey === start) return start ? [start] : [];
|
|
652
808
|
const target = toSlot(initialKey);
|
|
653
809
|
const page2 = flowPages.find((p) => p.key === target);
|
|
810
|
+
if (trustInitialKey) return page2 ? [target] : start ? [start] : [];
|
|
654
811
|
const guard = entryGuard(page2?.meta);
|
|
655
812
|
const ok = page2 && (!guard || evaluateGate(guard, funnel.snapshot()));
|
|
656
813
|
const first = ok ? target : start;
|
|
@@ -736,6 +893,44 @@ function FunnelView({
|
|
|
736
893
|
canGoBack: history.length > 1,
|
|
737
894
|
isNavigating
|
|
738
895
|
};
|
|
896
|
+
const bus = useBusInternal();
|
|
897
|
+
const navRef = useRef(value);
|
|
898
|
+
navRef.current = value;
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
if (!bus) return;
|
|
901
|
+
const control = {
|
|
902
|
+
goTo: (key) => {
|
|
903
|
+
if (!components.has(toRenderKey(key))) return false;
|
|
904
|
+
navRef.current.go(key);
|
|
905
|
+
return true;
|
|
906
|
+
},
|
|
907
|
+
goNext: () => navRef.current.next(),
|
|
908
|
+
goPrevious: () => navRef.current.back(),
|
|
909
|
+
snapshot: () => {
|
|
910
|
+
const v = navRef.current;
|
|
911
|
+
return {
|
|
912
|
+
key: v.page.key,
|
|
913
|
+
index: v.page.index,
|
|
914
|
+
total: v.page.total,
|
|
915
|
+
progressPercentage: v.page.progressPercentage,
|
|
916
|
+
canGoBack: v.canGoBack,
|
|
917
|
+
nextCandidates: v.nextCandidates
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
return bus.registerNavigation(control);
|
|
922
|
+
}, [bus]);
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
const v = navRef.current;
|
|
925
|
+
bus?.emit("navigation", {
|
|
926
|
+
key: v.page.key,
|
|
927
|
+
index: v.page.index,
|
|
928
|
+
total: v.page.total,
|
|
929
|
+
progressPercentage: v.page.progressPercentage,
|
|
930
|
+
canGoBack: v.canGoBack,
|
|
931
|
+
nextCandidates: v.nextCandidates
|
|
932
|
+
});
|
|
933
|
+
}, [bus, currentKey]);
|
|
739
934
|
const renderKey = toRenderKey(currentKey);
|
|
740
935
|
const Current = components.get(renderKey);
|
|
741
936
|
const pageEl = createElement(Suspense, { fallback: fallback ?? null }, Current ? createElement(Current) : null);
|
|
@@ -751,10 +946,85 @@ function useNavigation() {
|
|
|
751
946
|
if (!ctx) throw new Error("useNavigation must be used inside <FunnelView>");
|
|
752
947
|
return ctx;
|
|
753
948
|
}
|
|
949
|
+
var EMBED_CAPABILITIES = {
|
|
950
|
+
methods: ["nav.snapshot", "nav.goTo", "nav.goNext", "nav.goPrevious", "screenshot"],
|
|
951
|
+
events: ["navigation"]
|
|
952
|
+
};
|
|
953
|
+
function EmbedBridge() {
|
|
954
|
+
const bus = useBusInternal();
|
|
955
|
+
useEffect(() => {
|
|
956
|
+
if (!bus || typeof window === "undefined" || window.parent === window) return;
|
|
957
|
+
const parent = window.parent;
|
|
958
|
+
let disposed = false;
|
|
959
|
+
const send = (msg) => {
|
|
960
|
+
try {
|
|
961
|
+
parent.postMessage({ af: AF_PROTOCOL, source: "af-preview", ...msg }, "*");
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
const hello = () => send({ kind: "hello", role: "funnel", protocol: AF_PROTOCOL, capabilities: EMBED_CAPABILITIES });
|
|
966
|
+
const sendSnapshot = () => {
|
|
967
|
+
const s = bus.navigation();
|
|
968
|
+
if (s) send({ kind: "event", event: "navigation", data: s });
|
|
969
|
+
};
|
|
970
|
+
async function invoke(method, params) {
|
|
971
|
+
switch (method) {
|
|
972
|
+
case "nav.snapshot":
|
|
973
|
+
return bus.navigation();
|
|
974
|
+
case "nav.goTo": {
|
|
975
|
+
const key = params?.key;
|
|
976
|
+
if (typeof key !== "string" || !bus.goTo(key)) throw { code: "bad_params", message: `unknown page key: ${String(key)}` };
|
|
977
|
+
return void 0;
|
|
978
|
+
}
|
|
979
|
+
case "nav.goNext":
|
|
980
|
+
bus.goNext();
|
|
981
|
+
return void 0;
|
|
982
|
+
case "nav.goPrevious":
|
|
983
|
+
bus.goPrevious();
|
|
984
|
+
return void 0;
|
|
985
|
+
case "screenshot":
|
|
986
|
+
return await captureScreenshot(params ?? {});
|
|
987
|
+
default:
|
|
988
|
+
throw { code: "unknown_method", message: `unknown method: ${method}` };
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async function onMsg(e) {
|
|
992
|
+
if (e.source !== parent) return;
|
|
993
|
+
const d = e.data;
|
|
994
|
+
if (!d || typeof d !== "object" || d.af !== AF_PROTOCOL || d.source !== "af-host") return;
|
|
995
|
+
if (d.kind === "hello") {
|
|
996
|
+
hello();
|
|
997
|
+
sendSnapshot();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (d.kind !== "request" || typeof d.id !== "string") return;
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await invoke(d.method, d.params);
|
|
1003
|
+
if (!disposed) send({ kind: "response", id: d.id, ok: true, result });
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
const e2 = err;
|
|
1006
|
+
const error = e2 && typeof e2.code === "string" ? { code: e2.code, message: String(e2.message ?? e2.code) } : { code: "failed", message: String(e2?.message ?? err) };
|
|
1007
|
+
if (!disposed) send({ kind: "response", id: d.id, ok: false, error });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
window.addEventListener("message", onMsg);
|
|
1011
|
+
const offNav = bus.on("navigation", (data) => {
|
|
1012
|
+
if (!disposed) send({ kind: "event", event: "navigation", data });
|
|
1013
|
+
});
|
|
1014
|
+
hello();
|
|
1015
|
+
sendSnapshot();
|
|
1016
|
+
return () => {
|
|
1017
|
+
disposed = true;
|
|
1018
|
+
offNav();
|
|
1019
|
+
window.removeEventListener("message", onMsg);
|
|
1020
|
+
};
|
|
1021
|
+
}, [bus]);
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
754
1024
|
function usePage() {
|
|
755
1025
|
return useNavigation().page;
|
|
756
1026
|
}
|
|
757
|
-
var symModalId = /* @__PURE__ */ Symbol("
|
|
1027
|
+
var symModalId = /* @__PURE__ */ Symbol("AppfunnelModalId");
|
|
758
1028
|
var initialState = {};
|
|
759
1029
|
var ModalStateContext = createContext(initialState);
|
|
760
1030
|
var ModalIdContext = createContext(null);
|
|
@@ -769,7 +1039,9 @@ var runtimeStack = [];
|
|
|
769
1039
|
var dispatch = (action) => {
|
|
770
1040
|
const top = runtimeStack[runtimeStack.length - 1];
|
|
771
1041
|
if (!top) {
|
|
772
|
-
throw new Error(
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
"No modal dispatch \u2014 render inside <FunnelProvider> (which mounts the modal runtime)."
|
|
1044
|
+
);
|
|
773
1045
|
}
|
|
774
1046
|
top(action);
|
|
775
1047
|
};
|
|
@@ -868,26 +1140,42 @@ function useModal(modal, args) {
|
|
|
868
1140
|
const d = useContext(ModalDispatchContext) ?? dispatch;
|
|
869
1141
|
const isComponent = !!modal && typeof modal !== "string";
|
|
870
1142
|
const id = modal ? idOf(modal) : contextId;
|
|
871
|
-
if (!id)
|
|
1143
|
+
if (!id)
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
"useModal: no modal id (call inside a defineModal component, or pass an id/component)."
|
|
1146
|
+
);
|
|
872
1147
|
useEffect(() => {
|
|
873
|
-
if (isComponent && !REGISTRY[id])
|
|
1148
|
+
if (isComponent && !REGISTRY[id])
|
|
1149
|
+
registerModal(id, modal, args);
|
|
874
1150
|
}, [isComponent, id, modal, args]);
|
|
875
1151
|
const info = modals[id];
|
|
876
|
-
const show = useCallback(
|
|
1152
|
+
const show = useCallback(
|
|
1153
|
+
(a) => showModalVia(d, id, a),
|
|
1154
|
+
[d, id]
|
|
1155
|
+
);
|
|
877
1156
|
const hide = useCallback(() => hideModalVia(d, id), [d, id]);
|
|
878
1157
|
const remove = useCallback(() => removeModalVia(d, id), [d, id]);
|
|
879
|
-
const resolve = useCallback(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1158
|
+
const resolve = useCallback(
|
|
1159
|
+
(v) => {
|
|
1160
|
+
modalCallbacks[id]?.resolve(v);
|
|
1161
|
+
delete modalCallbacks[id];
|
|
1162
|
+
},
|
|
1163
|
+
[id]
|
|
1164
|
+
);
|
|
1165
|
+
const reject = useCallback(
|
|
1166
|
+
(v) => {
|
|
1167
|
+
modalCallbacks[id]?.reject(v);
|
|
1168
|
+
delete modalCallbacks[id];
|
|
1169
|
+
},
|
|
1170
|
+
[id]
|
|
1171
|
+
);
|
|
1172
|
+
const resolveHide = useCallback(
|
|
1173
|
+
(v) => {
|
|
1174
|
+
hideCallbacks[id]?.resolve(v);
|
|
1175
|
+
delete hideCallbacks[id];
|
|
1176
|
+
},
|
|
1177
|
+
[id]
|
|
1178
|
+
);
|
|
891
1179
|
return useMemo(
|
|
892
1180
|
() => ({
|
|
893
1181
|
id,
|
|
@@ -901,11 +1189,27 @@ function useModal(modal, args) {
|
|
|
901
1189
|
reject,
|
|
902
1190
|
resolveHide
|
|
903
1191
|
}),
|
|
904
|
-
[
|
|
1192
|
+
[
|
|
1193
|
+
id,
|
|
1194
|
+
info?.args,
|
|
1195
|
+
info?.visible,
|
|
1196
|
+
info?.keepMounted,
|
|
1197
|
+
show,
|
|
1198
|
+
hide,
|
|
1199
|
+
remove,
|
|
1200
|
+
resolve,
|
|
1201
|
+
reject,
|
|
1202
|
+
resolveHide
|
|
1203
|
+
]
|
|
905
1204
|
);
|
|
906
1205
|
}
|
|
907
1206
|
function defineModal(Comp) {
|
|
908
|
-
return function Wrapped({
|
|
1207
|
+
return function Wrapped({
|
|
1208
|
+
id,
|
|
1209
|
+
defaultVisible,
|
|
1210
|
+
keepMounted,
|
|
1211
|
+
...props
|
|
1212
|
+
}) {
|
|
909
1213
|
const { args, show } = useModal(id);
|
|
910
1214
|
const modals = useContext(ModalStateContext);
|
|
911
1215
|
const d = useContext(ModalDispatchContext) ?? dispatch;
|
|
@@ -938,7 +1242,9 @@ function ModalRoot() {
|
|
|
938
1242
|
return createElement(
|
|
939
1243
|
"div",
|
|
940
1244
|
{ "data-appfunnel-modal-root": "" },
|
|
941
|
-
...toRender.map(
|
|
1245
|
+
...toRender.map(
|
|
1246
|
+
(t) => createElement(t.comp, { key: t.id, id: t.id, ...t.props })
|
|
1247
|
+
)
|
|
942
1248
|
);
|
|
943
1249
|
}
|
|
944
1250
|
function ModalRuntime({ children }) {
|
|
@@ -1066,12 +1372,20 @@ function Modal(props) {
|
|
|
1066
1372
|
|
|
1067
1373
|
// src/commerce/checkout.tsx
|
|
1068
1374
|
function createMockDriver(provider = "stripe", options = {}) {
|
|
1069
|
-
const success = options.result ?? {
|
|
1070
|
-
|
|
1375
|
+
const success = options.result ?? {
|
|
1376
|
+
ok: true,
|
|
1377
|
+
amountMinor: 0,
|
|
1378
|
+
currency: "USD"
|
|
1379
|
+
};
|
|
1380
|
+
const offError = typeof options.failOffSession === "object" ? options.failOffSession : checkoutError(
|
|
1381
|
+
"authentication_required",
|
|
1382
|
+
"off-session charge declined (mock)"
|
|
1383
|
+
);
|
|
1071
1384
|
return {
|
|
1072
1385
|
provider,
|
|
1073
1386
|
start: (req) => {
|
|
1074
|
-
if (options.failOffSession && !req.surface)
|
|
1387
|
+
if (options.failOffSession && !req.surface)
|
|
1388
|
+
return Promise.resolve({ ok: false, error: offError });
|
|
1075
1389
|
return Promise.resolve(success);
|
|
1076
1390
|
},
|
|
1077
1391
|
renderInline: (req, cb) => createElement(
|
|
@@ -1082,7 +1396,13 @@ function createMockDriver(provider = "stripe", options = {}) {
|
|
|
1082
1396
|
onClick: () => {
|
|
1083
1397
|
cb.onStart?.();
|
|
1084
1398
|
if (success.ok) cb.onSuccess(success);
|
|
1085
|
-
else
|
|
1399
|
+
else
|
|
1400
|
+
cb.onError(
|
|
1401
|
+
success.error ?? checkoutError(
|
|
1402
|
+
"processing_error",
|
|
1403
|
+
"failed (mock)"
|
|
1404
|
+
)
|
|
1405
|
+
);
|
|
1086
1406
|
}
|
|
1087
1407
|
},
|
|
1088
1408
|
`Pay (mock ${req.surface})`
|
|
@@ -1140,11 +1460,11 @@ function useCheckout(opts = {}) {
|
|
|
1140
1460
|
[catalog, kind]
|
|
1141
1461
|
);
|
|
1142
1462
|
const recovering = useRef(false);
|
|
1143
|
-
const failRef = useRef(
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
async () => ({ ok: false })
|
|
1463
|
+
const failRef = useRef(
|
|
1464
|
+
() => {
|
|
1465
|
+
}
|
|
1147
1466
|
);
|
|
1467
|
+
const attemptRef = useRef(async () => ({ ok: false }));
|
|
1148
1468
|
const succeed = useCallback(
|
|
1149
1469
|
(result, product) => {
|
|
1150
1470
|
setStatus("success");
|
|
@@ -1158,7 +1478,12 @@ function useCheckout(opts = {}) {
|
|
|
1158
1478
|
// server-minted (browser↔CAPI dedup); undefined if none
|
|
1159
1479
|
});
|
|
1160
1480
|
if (isSubscription) {
|
|
1161
|
-
tracker.track("subscription.created", {
|
|
1481
|
+
tracker.track("subscription.created", {
|
|
1482
|
+
amount,
|
|
1483
|
+
currency: result.currency,
|
|
1484
|
+
productId,
|
|
1485
|
+
eventId: result.eventId
|
|
1486
|
+
});
|
|
1162
1487
|
}
|
|
1163
1488
|
opts.onSuccess?.(result);
|
|
1164
1489
|
if (advance) nav.next();
|
|
@@ -1172,14 +1497,25 @@ function useCheckout(opts = {}) {
|
|
|
1172
1497
|
setStatus("loading");
|
|
1173
1498
|
setError(null);
|
|
1174
1499
|
const { productId, amount, currency } = productFacts(product);
|
|
1175
|
-
tracker.track("checkout.start", {
|
|
1500
|
+
tracker.track("checkout.start", {
|
|
1501
|
+
productId,
|
|
1502
|
+
amount,
|
|
1503
|
+
currency,
|
|
1504
|
+
surface,
|
|
1505
|
+
eventId: newEventId()
|
|
1506
|
+
});
|
|
1176
1507
|
},
|
|
1177
1508
|
[tracker, productFacts]
|
|
1178
1509
|
);
|
|
1179
1510
|
const paymentAdded = useCallback(
|
|
1180
1511
|
(product) => {
|
|
1181
1512
|
const { productId, amount, currency } = productFacts(product);
|
|
1182
|
-
tracker.track("checkout.payment_added", {
|
|
1513
|
+
tracker.track("checkout.payment_added", {
|
|
1514
|
+
productId,
|
|
1515
|
+
amount,
|
|
1516
|
+
currency,
|
|
1517
|
+
eventId: newEventId()
|
|
1518
|
+
});
|
|
1183
1519
|
},
|
|
1184
1520
|
[tracker, productFacts]
|
|
1185
1521
|
);
|
|
@@ -1202,7 +1538,9 @@ function useCheckout(opts = {}) {
|
|
|
1202
1538
|
}
|
|
1203
1539
|
};
|
|
1204
1540
|
failRef.current = (err, product) => {
|
|
1205
|
-
setStatus(
|
|
1541
|
+
setStatus(
|
|
1542
|
+
err.category === "authentication_required" ? "requires_action" : "error"
|
|
1543
|
+
);
|
|
1206
1544
|
setError(err);
|
|
1207
1545
|
tracker.track("checkout.failed", {
|
|
1208
1546
|
category: err.category,
|
|
@@ -1233,35 +1571,59 @@ function useCheckout(opts = {}) {
|
|
|
1233
1571
|
if (!product) return;
|
|
1234
1572
|
recovering.current = true;
|
|
1235
1573
|
if (!surface) {
|
|
1236
|
-
if (intent === "upsell")
|
|
1237
|
-
|
|
1574
|
+
if (intent === "upsell")
|
|
1575
|
+
void attemptRef.current(product);
|
|
1576
|
+
else
|
|
1577
|
+
startRecovery(
|
|
1578
|
+
product,
|
|
1579
|
+
PROVIDER_PROFILES[driver.provider]?.surfaces.sheet ? "sheet" : "popup"
|
|
1580
|
+
);
|
|
1238
1581
|
return;
|
|
1239
1582
|
}
|
|
1240
1583
|
startRecovery(product, surface);
|
|
1241
1584
|
}
|
|
1242
1585
|
};
|
|
1243
|
-
void showModal(
|
|
1586
|
+
void showModal(
|
|
1587
|
+
route.modal,
|
|
1588
|
+
props
|
|
1589
|
+
);
|
|
1244
1590
|
} else if ("fallback" in route) {
|
|
1245
|
-
if (product && !recovering.current)
|
|
1591
|
+
if (product && !recovering.current)
|
|
1592
|
+
startRecovery(product, route.fallback);
|
|
1246
1593
|
}
|
|
1247
1594
|
};
|
|
1248
1595
|
attemptRef.current = async (product, surface) => {
|
|
1249
1596
|
begin(product, surface);
|
|
1250
1597
|
try {
|
|
1251
|
-
const result = await driver.start({
|
|
1598
|
+
const result = await driver.start({
|
|
1599
|
+
product,
|
|
1600
|
+
intent,
|
|
1601
|
+
surface,
|
|
1602
|
+
kind
|
|
1603
|
+
});
|
|
1252
1604
|
if (result.ok) succeedRef.current(result, product);
|
|
1253
|
-
else
|
|
1605
|
+
else
|
|
1606
|
+
failRef.current(
|
|
1607
|
+
result.error ?? checkoutError("unknown", "Checkout failed"),
|
|
1608
|
+
product
|
|
1609
|
+
);
|
|
1254
1610
|
return result;
|
|
1255
1611
|
} catch (e) {
|
|
1256
|
-
const err = checkoutError(
|
|
1612
|
+
const err = checkoutError(
|
|
1613
|
+
"processing_error",
|
|
1614
|
+
e instanceof Error ? e.message : "Checkout failed"
|
|
1615
|
+
);
|
|
1257
1616
|
failRef.current(err, product);
|
|
1258
1617
|
return { ok: false, error: err };
|
|
1259
1618
|
}
|
|
1260
1619
|
};
|
|
1261
|
-
const open = useCallback(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1620
|
+
const open = useCallback(
|
|
1621
|
+
async (product, surface) => {
|
|
1622
|
+
recovering.current = false;
|
|
1623
|
+
return attemptRef.current(product, surface);
|
|
1624
|
+
},
|
|
1625
|
+
[]
|
|
1626
|
+
);
|
|
1265
1627
|
const callbacksFor = useCallback(
|
|
1266
1628
|
(product, surface) => ({
|
|
1267
1629
|
onStart: () => {
|
|
@@ -1279,7 +1641,14 @@ function useCheckout(opts = {}) {
|
|
|
1279
1641
|
setStatus("idle");
|
|
1280
1642
|
setError(null);
|
|
1281
1643
|
}, []);
|
|
1282
|
-
return {
|
|
1644
|
+
return {
|
|
1645
|
+
open,
|
|
1646
|
+
status,
|
|
1647
|
+
error,
|
|
1648
|
+
isLoading: status === "loading",
|
|
1649
|
+
reset,
|
|
1650
|
+
callbacksFor
|
|
1651
|
+
};
|
|
1283
1652
|
}
|
|
1284
1653
|
var CheckoutSheet = defineModal(({ product, surface, intent, kind, onSuccess, onError, onPaymentAdded }) => {
|
|
1285
1654
|
const driver = useDriver();
|
|
@@ -1292,7 +1661,10 @@ var CheckoutSheet = defineModal(({ product, surface, intent, kind, onSuccess, on
|
|
|
1292
1661
|
if (!hostable) {
|
|
1293
1662
|
resolved.current = true;
|
|
1294
1663
|
onErrorRef.current(
|
|
1295
|
-
checkoutError(
|
|
1664
|
+
checkoutError(
|
|
1665
|
+
"processing_error",
|
|
1666
|
+
"This checkout driver has no inline surface to host in the sheet"
|
|
1667
|
+
)
|
|
1296
1668
|
);
|
|
1297
1669
|
void modal.hide();
|
|
1298
1670
|
modal.remove();
|
|
@@ -1300,7 +1672,12 @@ var CheckoutSheet = defineModal(({ product, surface, intent, kind, onSuccess, on
|
|
|
1300
1672
|
}
|
|
1301
1673
|
return () => {
|
|
1302
1674
|
if (!resolved.current) {
|
|
1303
|
-
onErrorRef.current(
|
|
1675
|
+
onErrorRef.current(
|
|
1676
|
+
checkoutError(
|
|
1677
|
+
"canceled",
|
|
1678
|
+
"Checkout was dismissed before completing"
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1304
1681
|
}
|
|
1305
1682
|
};
|
|
1306
1683
|
}, []);
|
|
@@ -1327,21 +1704,31 @@ var CheckoutSheet = defineModal(({ product, surface, intent, kind, onSuccess, on
|
|
|
1327
1704
|
)
|
|
1328
1705
|
});
|
|
1329
1706
|
});
|
|
1330
|
-
function trigger(onClick, {
|
|
1707
|
+
function trigger(onClick, {
|
|
1708
|
+
asChild,
|
|
1709
|
+
children,
|
|
1710
|
+
className,
|
|
1711
|
+
disabled
|
|
1712
|
+
}) {
|
|
1331
1713
|
if (asChild && isValidElement(children)) {
|
|
1332
1714
|
const child = children;
|
|
1333
1715
|
return cloneElement(child, { onClick });
|
|
1334
1716
|
}
|
|
1335
|
-
return createElement(
|
|
1717
|
+
return createElement(
|
|
1718
|
+
"button",
|
|
1719
|
+
{ type: "button", className, disabled, onClick },
|
|
1720
|
+
children ?? "Continue"
|
|
1721
|
+
);
|
|
1336
1722
|
}
|
|
1337
1723
|
function Checkout({
|
|
1338
|
-
|
|
1724
|
+
offering,
|
|
1339
1725
|
surface,
|
|
1340
1726
|
asChild,
|
|
1341
1727
|
children,
|
|
1342
1728
|
className,
|
|
1343
1729
|
...options
|
|
1344
1730
|
}) {
|
|
1731
|
+
const product = offering;
|
|
1345
1732
|
const driver = useDriver();
|
|
1346
1733
|
const checkout = useCheckout(options);
|
|
1347
1734
|
const intent = options.intent ?? "purchase";
|
|
@@ -1349,7 +1736,10 @@ function Checkout({
|
|
|
1349
1736
|
return driver.renderInline ? createElement(
|
|
1350
1737
|
"div",
|
|
1351
1738
|
{ key: product, className },
|
|
1352
|
-
driver.renderInline(
|
|
1739
|
+
driver.renderInline(
|
|
1740
|
+
{ product, intent, surface },
|
|
1741
|
+
checkout.callbacksFor(product, surface)
|
|
1742
|
+
)
|
|
1353
1743
|
) : null;
|
|
1354
1744
|
}
|
|
1355
1745
|
if (surface === "sheet") {
|
|
@@ -1366,7 +1756,12 @@ function Checkout({
|
|
|
1366
1756
|
onPaymentAdded: cb.onPaymentAdded
|
|
1367
1757
|
});
|
|
1368
1758
|
};
|
|
1369
|
-
return trigger(open, {
|
|
1759
|
+
return trigger(open, {
|
|
1760
|
+
asChild,
|
|
1761
|
+
children,
|
|
1762
|
+
className,
|
|
1763
|
+
disabled: checkout.isLoading
|
|
1764
|
+
});
|
|
1370
1765
|
}
|
|
1371
1766
|
return trigger(() => checkout.open(product, surface), {
|
|
1372
1767
|
asChild,
|
|
@@ -1375,18 +1770,29 @@ function Checkout({
|
|
|
1375
1770
|
disabled: checkout.isLoading
|
|
1376
1771
|
});
|
|
1377
1772
|
}
|
|
1378
|
-
function Upsell({
|
|
1773
|
+
function Upsell({
|
|
1774
|
+
offering,
|
|
1775
|
+
fallback,
|
|
1776
|
+
asChild,
|
|
1777
|
+
children,
|
|
1778
|
+
className,
|
|
1779
|
+
...options
|
|
1780
|
+
}) {
|
|
1781
|
+
const product = offering;
|
|
1379
1782
|
const checkout = useCheckout({
|
|
1380
1783
|
...options,
|
|
1381
1784
|
intent: "upsell",
|
|
1382
1785
|
recoverySurface: fallback ?? options.recoverySurface
|
|
1383
1786
|
});
|
|
1384
|
-
return trigger(
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1787
|
+
return trigger(
|
|
1788
|
+
() => void checkout.open(product),
|
|
1789
|
+
{
|
|
1790
|
+
asChild,
|
|
1791
|
+
children: children ?? "Add to my plan",
|
|
1792
|
+
className,
|
|
1793
|
+
disabled: checkout.isLoading
|
|
1794
|
+
}
|
|
1795
|
+
);
|
|
1390
1796
|
}
|
|
1391
1797
|
var consumedResumes = /* @__PURE__ */ new Set();
|
|
1392
1798
|
function CheckoutResume(options = {}) {
|
|
@@ -1404,10 +1810,19 @@ function CheckoutResume(options = {}) {
|
|
|
1404
1810
|
const cb = callbacksRef.current();
|
|
1405
1811
|
if (result.ok) {
|
|
1406
1812
|
url.searchParams.delete("af_checkout");
|
|
1407
|
-
window.history.replaceState(
|
|
1813
|
+
window.history.replaceState(
|
|
1814
|
+
window.history.state,
|
|
1815
|
+
"",
|
|
1816
|
+
url.toString()
|
|
1817
|
+
);
|
|
1408
1818
|
cb.onSuccess(result);
|
|
1409
1819
|
} else {
|
|
1410
|
-
cb.onError(
|
|
1820
|
+
cb.onError(
|
|
1821
|
+
result.error ?? checkoutError(
|
|
1822
|
+
"unknown",
|
|
1823
|
+
"The checkout could not be verified"
|
|
1824
|
+
)
|
|
1825
|
+
);
|
|
1411
1826
|
}
|
|
1412
1827
|
});
|
|
1413
1828
|
}, []);
|
|
@@ -2522,7 +2937,8 @@ var Namespace = class {
|
|
|
2522
2937
|
const out = {};
|
|
2523
2938
|
const all = this.store.getState();
|
|
2524
2939
|
for (const key in all) {
|
|
2525
|
-
if (key.startsWith(this.dot))
|
|
2940
|
+
if (key.startsWith(this.dot))
|
|
2941
|
+
out[key.slice(this.dot.length)] = all[key];
|
|
2526
2942
|
}
|
|
2527
2943
|
return out;
|
|
2528
2944
|
}
|
|
@@ -2568,9 +2984,10 @@ var FunnelCtx = createContext(null);
|
|
|
2568
2984
|
function FunnelProvider({
|
|
2569
2985
|
config,
|
|
2570
2986
|
sessionValues,
|
|
2987
|
+
deferSessionValues = false,
|
|
2571
2988
|
tracker,
|
|
2572
2989
|
context: contextOpts,
|
|
2573
|
-
|
|
2990
|
+
offerings,
|
|
2574
2991
|
experiments,
|
|
2575
2992
|
checkout,
|
|
2576
2993
|
messages,
|
|
@@ -2583,7 +3000,12 @@ function FunnelProvider({
|
|
|
2583
3000
|
const [value] = useState(() => {
|
|
2584
3001
|
const s = store ?? createFunnelStore(
|
|
2585
3002
|
{ responses: config?.responses, data: config?.data },
|
|
2586
|
-
|
|
3003
|
+
// FLASH-FREE HYDRATION: with deferSessionValues the initial render is seeded with
|
|
3004
|
+
// DEFAULTS ONLY, so the client's first render is byte-identical to the build-time
|
|
3005
|
+
// DEFAULT-state prerender it hydrates against (clean match). The visitor's saved
|
|
3006
|
+
// answers arrive as a post-hydration setMany below. Without the flag, sessionValues
|
|
3007
|
+
// seed the store at construction (SSR + non-prerendered paths).
|
|
3008
|
+
deferSessionValues ? void 0 : sessionValues
|
|
2587
3009
|
);
|
|
2588
3010
|
const ctx = buildContext({
|
|
2589
3011
|
funnelId: config?.id,
|
|
@@ -2601,7 +3023,7 @@ function FunnelProvider({
|
|
|
2601
3023
|
const value2 = s.get("user.email");
|
|
2602
3024
|
return typeof value2 === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value2) ? value2 : void 0;
|
|
2603
3025
|
};
|
|
2604
|
-
const catalog = buildCatalog(
|
|
3026
|
+
const catalog = buildCatalog(offerings ?? []);
|
|
2605
3027
|
return {
|
|
2606
3028
|
funnel: createFunnel(s, ctx, busTracker, experiments ?? []),
|
|
2607
3029
|
store: s,
|
|
@@ -2609,12 +3031,25 @@ function FunnelProvider({
|
|
|
2609
3031
|
catalog,
|
|
2610
3032
|
// Slot → catalog-key resolution wraps the email-enriching driver, so the wire charges the real
|
|
2611
3033
|
// catalog key while pages + analytics keep the durable SLOT identity (req.product).
|
|
2612
|
-
driver: driverWithCatalog(
|
|
3034
|
+
driver: driverWithCatalog(
|
|
3035
|
+
driverWithEmail(
|
|
3036
|
+
checkout ?? createMockDriver(),
|
|
3037
|
+
emailForCheckout
|
|
3038
|
+
),
|
|
3039
|
+
catalog
|
|
3040
|
+
),
|
|
2613
3041
|
tracker: busTracker,
|
|
2614
3042
|
bus
|
|
2615
3043
|
};
|
|
2616
3044
|
});
|
|
2617
3045
|
useEffect(() => attachBus(value.bus), [value.bus]);
|
|
3046
|
+
useEffect(() => {
|
|
3047
|
+
if (!deferSessionValues || !sessionValues) return;
|
|
3048
|
+
const updates = {};
|
|
3049
|
+
for (const [key, v] of Object.entries(sessionValues))
|
|
3050
|
+
if (v !== void 0) updates[key] = v;
|
|
3051
|
+
if (Object.keys(updates).length > 0) value.store.setMany(updates);
|
|
3052
|
+
}, []);
|
|
2618
3053
|
useEffect(() => {
|
|
2619
3054
|
value.tracker.setAcquisition?.(buildAcquisition(value.context));
|
|
2620
3055
|
}, [value]);
|
|
@@ -2643,13 +3078,18 @@ var FALLBACK_TRACKER = createConsoleTracker({ silent: true });
|
|
|
2643
3078
|
function useFunnelCtx() {
|
|
2644
3079
|
const ctx = useContext(FunnelCtx);
|
|
2645
3080
|
if (!ctx) {
|
|
2646
|
-
throw new Error(
|
|
3081
|
+
throw new Error(
|
|
3082
|
+
"useFunnel/useResponse/\u2026 must be used inside <FunnelProvider>"
|
|
3083
|
+
);
|
|
2647
3084
|
}
|
|
2648
3085
|
return ctx;
|
|
2649
3086
|
}
|
|
2650
3087
|
function useFunnel() {
|
|
2651
3088
|
return useFunnelCtx().funnel;
|
|
2652
3089
|
}
|
|
3090
|
+
function useBusInternal() {
|
|
3091
|
+
return useContext(FunnelCtx)?.bus ?? null;
|
|
3092
|
+
}
|
|
2653
3093
|
function useStoreKey(store, key) {
|
|
2654
3094
|
const subscribe = useCallback(
|
|
2655
3095
|
(cb) => store.subscribe(cb, { keys: [key] }),
|
|
@@ -2678,7 +3118,9 @@ function useField(path) {
|
|
|
2678
3118
|
const key = path.slice(dot + 1);
|
|
2679
3119
|
const { funnel, store } = useFunnelCtx();
|
|
2680
3120
|
if (!(ns in NAMESPACE_PREFIX)) {
|
|
2681
|
-
throw new Error(
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`useField: "${path}" is not a writable namespace (user/responses/data)`
|
|
3123
|
+
);
|
|
2682
3124
|
}
|
|
2683
3125
|
const value = useStoreKey(store, `${NAMESPACE_PREFIX[ns]}.${key}`);
|
|
2684
3126
|
const set = useCallback(
|
|
@@ -2708,11 +3150,160 @@ function useSystem() {
|
|
|
2708
3150
|
function useContextValue(path) {
|
|
2709
3151
|
const ctx = useContextObject();
|
|
2710
3152
|
return useMemo(
|
|
2711
|
-
() => path.split(".").reduce(
|
|
3153
|
+
() => path.split(".").reduce(
|
|
3154
|
+
(acc, k) => acc?.[k],
|
|
3155
|
+
ctx
|
|
3156
|
+
),
|
|
2712
3157
|
[ctx, path]
|
|
2713
3158
|
);
|
|
2714
3159
|
}
|
|
2715
3160
|
|
|
3161
|
+
// src/tracking/snapshotPersistence.ts
|
|
3162
|
+
function createSnapshotPersistence(opts) {
|
|
3163
|
+
const baseDelay = opts.baseDelayMs ?? 1e3;
|
|
3164
|
+
const maxDelay = opts.maxDelayMs ?? 3e4;
|
|
3165
|
+
const setT = opts.timers?.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
3166
|
+
const clearT = opts.timers?.clearTimeout ?? ((h) => clearTimeout(h));
|
|
3167
|
+
const rand = opts.random ?? Math.random;
|
|
3168
|
+
let pending = null;
|
|
3169
|
+
let seq = 0;
|
|
3170
|
+
let pendingSeq = 0;
|
|
3171
|
+
let inFlight = false;
|
|
3172
|
+
let attempt = 0;
|
|
3173
|
+
let retryTimer = null;
|
|
3174
|
+
let disposed = false;
|
|
3175
|
+
let fatalSeq = -1;
|
|
3176
|
+
const clearRetry = () => {
|
|
3177
|
+
if (retryTimer !== null) {
|
|
3178
|
+
clearT(retryTimer);
|
|
3179
|
+
retryTimer = null;
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
const buildBody = (snapshot, s) => JSON.stringify({ sessionId: opts.getSessionId(), userData: snapshot, seq: s });
|
|
3183
|
+
const trySend = () => {
|
|
3184
|
+
if (disposed || inFlight || pending === null) return;
|
|
3185
|
+
if (retryTimer !== null) return;
|
|
3186
|
+
const sessionId = opts.getSessionId();
|
|
3187
|
+
if (!sessionId) return;
|
|
3188
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return;
|
|
3189
|
+
if (pendingSeq === fatalSeq) return;
|
|
3190
|
+
const snapshot = pending;
|
|
3191
|
+
const sentSeq = pendingSeq;
|
|
3192
|
+
const body = buildBody(snapshot, sentSeq);
|
|
3193
|
+
inFlight = true;
|
|
3194
|
+
void fetch(opts.url, {
|
|
3195
|
+
method: "POST",
|
|
3196
|
+
headers: { "content-type": "application/json" },
|
|
3197
|
+
body,
|
|
3198
|
+
keepalive: true
|
|
3199
|
+
}).then((res) => {
|
|
3200
|
+
inFlight = false;
|
|
3201
|
+
if (disposed) return;
|
|
3202
|
+
if (res.ok) {
|
|
3203
|
+
attempt = 0;
|
|
3204
|
+
if (pendingSeq === sentSeq) pending = null;
|
|
3205
|
+
else trySend();
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
if (res.status >= 400 && res.status < 500) {
|
|
3209
|
+
if (process.env.NODE_ENV !== "production") {
|
|
3210
|
+
console.warn(
|
|
3211
|
+
`[appfunnel] snapshot persist rejected ${res.status} (dropping snapshot; check session/payload)`
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
fatalSeq = sentSeq;
|
|
3215
|
+
if (pendingSeq === sentSeq) pending = null;
|
|
3216
|
+
else trySend();
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
scheduleRetry();
|
|
3220
|
+
}).catch(() => {
|
|
3221
|
+
inFlight = false;
|
|
3222
|
+
if (disposed) return;
|
|
3223
|
+
scheduleRetry();
|
|
3224
|
+
});
|
|
3225
|
+
};
|
|
3226
|
+
const scheduleRetry = () => {
|
|
3227
|
+
if (disposed || pending === null) return;
|
|
3228
|
+
const exp = Math.min(maxDelay, baseDelay * 2 ** attempt);
|
|
3229
|
+
const delay2 = Math.round(exp * (0.5 + rand() * 0.5));
|
|
3230
|
+
attempt++;
|
|
3231
|
+
clearRetry();
|
|
3232
|
+
retryTimer = setT(() => {
|
|
3233
|
+
retryTimer = null;
|
|
3234
|
+
trySend();
|
|
3235
|
+
}, delay2);
|
|
3236
|
+
};
|
|
3237
|
+
const push = (snapshot) => {
|
|
3238
|
+
if (disposed) return;
|
|
3239
|
+
pending = snapshot;
|
|
3240
|
+
pendingSeq = ++seq;
|
|
3241
|
+
if (fatalSeq !== -1 && pendingSeq !== fatalSeq) fatalSeq = -1;
|
|
3242
|
+
if (retryTimer === null) trySend();
|
|
3243
|
+
};
|
|
3244
|
+
const flushBeacon = () => {
|
|
3245
|
+
if (disposed || pending === null) return;
|
|
3246
|
+
const sessionId = opts.getSessionId();
|
|
3247
|
+
if (!sessionId) return;
|
|
3248
|
+
const body = buildBody(pending, pendingSeq);
|
|
3249
|
+
let delivered = false;
|
|
3250
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
3251
|
+
try {
|
|
3252
|
+
delivered = navigator.sendBeacon(
|
|
3253
|
+
opts.url,
|
|
3254
|
+
new Blob([body], { type: "application/json" })
|
|
3255
|
+
);
|
|
3256
|
+
} catch {
|
|
3257
|
+
delivered = false;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
if (!delivered) {
|
|
3261
|
+
try {
|
|
3262
|
+
void fetch(opts.url, {
|
|
3263
|
+
method: "POST",
|
|
3264
|
+
headers: { "content-type": "application/json" },
|
|
3265
|
+
body,
|
|
3266
|
+
keepalive: true
|
|
3267
|
+
}).catch(() => {
|
|
3268
|
+
});
|
|
3269
|
+
delivered = true;
|
|
3270
|
+
} catch {
|
|
3271
|
+
delivered = false;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
if (delivered) pending = null;
|
|
3275
|
+
};
|
|
3276
|
+
const onHidden = () => {
|
|
3277
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
3278
|
+
flushBeacon();
|
|
3279
|
+
}
|
|
3280
|
+
};
|
|
3281
|
+
const onPageHide = () => flushBeacon();
|
|
3282
|
+
const onOnline = () => trySend();
|
|
3283
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
3284
|
+
try {
|
|
3285
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
3286
|
+
window.addEventListener("pagehide", onPageHide);
|
|
3287
|
+
window.addEventListener("online", onOnline);
|
|
3288
|
+
} catch {
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
const dispose = () => {
|
|
3292
|
+
disposed = true;
|
|
3293
|
+
clearRetry();
|
|
3294
|
+
pending = null;
|
|
3295
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
3296
|
+
try {
|
|
3297
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
3298
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
3299
|
+
window.removeEventListener("online", onOnline);
|
|
3300
|
+
} catch {
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
return { push, flushBeacon, dispose };
|
|
3305
|
+
}
|
|
3306
|
+
|
|
2716
3307
|
// src/tracking/platformTracker.ts
|
|
2717
3308
|
function validClickIds(acq) {
|
|
2718
3309
|
return Object.entries(acq.clickIds ?? {}).filter(
|
|
@@ -2818,32 +3409,35 @@ function createPlatformTracker(cfg) {
|
|
|
2818
3409
|
}
|
|
2819
3410
|
};
|
|
2820
3411
|
const VARS_DEBOUNCE_MS = 800;
|
|
3412
|
+
const dataUrl = `${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event/session/data`;
|
|
2821
3413
|
let pendingVars = null;
|
|
2822
3414
|
let varsTimer = null;
|
|
3415
|
+
const persistence = createSnapshotPersistence({
|
|
3416
|
+
url: dataUrl,
|
|
3417
|
+
getSessionId: () => sessionId
|
|
3418
|
+
});
|
|
2823
3419
|
const flushVars = () => {
|
|
2824
3420
|
if (varsTimer) {
|
|
2825
3421
|
clearTimeout(varsTimer);
|
|
2826
3422
|
varsTimer = null;
|
|
2827
3423
|
}
|
|
2828
3424
|
if (!pendingVars) return;
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
};
|
|
2842
|
-
if (sessionId) doPost();
|
|
2843
|
-
else chain = chain.then(doPost);
|
|
3425
|
+
const snapshot = pendingVars;
|
|
3426
|
+
pendingVars = null;
|
|
3427
|
+
persistence.push(snapshot);
|
|
3428
|
+
if (!sessionId) chain = chain.then(() => {
|
|
3429
|
+
persistence.push(snapshot);
|
|
3430
|
+
});
|
|
3431
|
+
};
|
|
3432
|
+
const drainOnExit = () => {
|
|
3433
|
+
if (pendingVars) {
|
|
3434
|
+
flushVars();
|
|
3435
|
+
persistence.flushBeacon();
|
|
3436
|
+
}
|
|
2844
3437
|
};
|
|
2845
3438
|
try {
|
|
2846
|
-
window.addEventListener("
|
|
3439
|
+
window.addEventListener("visibilitychange", drainOnExit);
|
|
3440
|
+
window.addEventListener("pagehide", drainOnExit);
|
|
2847
3441
|
} catch {
|
|
2848
3442
|
}
|
|
2849
3443
|
if (cfg.experiment?.id && cfg.experiment.variant) {
|
|
@@ -2886,6 +3480,7 @@ function createFunnelTree({
|
|
|
2886
3480
|
}) {
|
|
2887
3481
|
return function tree(opts) {
|
|
2888
3482
|
const mode = opts.mode ?? "live";
|
|
3483
|
+
const embedEnabled = opts.embed ?? false;
|
|
2889
3484
|
const tracker = opts.tracking ? createPlatformTracker({
|
|
2890
3485
|
...opts.tracking,
|
|
2891
3486
|
visitorId: opts.visitorId,
|
|
@@ -2907,6 +3502,7 @@ function createFunnelTree({
|
|
|
2907
3502
|
Fragment,
|
|
2908
3503
|
null,
|
|
2909
3504
|
createElement(CheckoutResume, null),
|
|
3505
|
+
embedEnabled ? createElement(EmbedBridge, null) : null,
|
|
2910
3506
|
layout ? createElement(layout, null, children) : children
|
|
2911
3507
|
);
|
|
2912
3508
|
};
|
|
@@ -2914,12 +3510,13 @@ function createFunnelTree({
|
|
|
2914
3510
|
assets: opts.assets,
|
|
2915
3511
|
children: createElement(FunnelProvider, {
|
|
2916
3512
|
config,
|
|
2917
|
-
|
|
3513
|
+
offerings: opts.offerings ?? [],
|
|
2918
3514
|
messages: opts.messages ?? messages,
|
|
2919
3515
|
locale: opts.locale,
|
|
2920
3516
|
tracker,
|
|
2921
3517
|
checkout,
|
|
2922
3518
|
sessionValues: opts.sessionValues,
|
|
3519
|
+
deferSessionValues: opts.deferSessionValues,
|
|
2923
3520
|
experiments: opts.experiments,
|
|
2924
3521
|
context: {
|
|
2925
3522
|
mode,
|
|
@@ -2931,6 +3528,13 @@ function createFunnelTree({
|
|
|
2931
3528
|
pages,
|
|
2932
3529
|
layout: layoutWithResume,
|
|
2933
3530
|
initialKey: opts.initialKey,
|
|
3531
|
+
// FLASH-FREE HYDRATION (Option B, FIX 1): trust the SERVER-resolved initialKey instead of
|
|
3532
|
+
// re-running the entry guard. The CLIENT sets this via deferSessionValues (its deferred
|
|
3533
|
+
// snapshot is empty, so re-running the guard would bounce a qualified deep-link and
|
|
3534
|
+
// mismatch the markup). The build-time PRERENDER sets `trustInitialKey` explicitly so a
|
|
3535
|
+
// guarded page prerenders its OWN default-state markup (the renderer picks target-vs-bounce
|
|
3536
|
+
// at serve time). Live SSR fallback leaves both off → the guard runs against real values.
|
|
3537
|
+
trustInitialKey: opts.trustInitialKey ?? opts.deferSessionValues,
|
|
2934
3538
|
prefetch: "off"
|
|
2935
3539
|
// renderer background-modulepreloads all chunks
|
|
2936
3540
|
})
|
|
@@ -3077,6 +3681,6 @@ function Script({
|
|
|
3077
3681
|
return null;
|
|
3078
3682
|
}
|
|
3079
3683
|
|
|
3080
|
-
export { AssetsContext, AssetsProvider, Back, Checkout, CheckoutResume, Choice, EVENT_CATALOG, FunnelProvider, FunnelToaster, FunnelView, Modal, Next, Script, Sheet, Upsell, VariableStore, attachBus, buildAcquisition, buildContext, createBus, createConsoleTracker, createFunnelStore, createFunnelTree, createMockDriver, createPlatformTracker, defineModal, driverWithEmail, hideModal, integrationEvents, isIntegrationEvent, isTrackableEvent, newEventId, registerModal, removeModal, showModal, toast, unregisterModal, useActiveLocale, useAsset, useAssets, useCheckout, useClickIds, useContextValue, useData, useDevice, useExperiment, useField, useFunnel, useLocale, useModal, useNavigation,
|
|
3684
|
+
export { AssetsContext, AssetsProvider, Back, Checkout, CheckoutResume, Choice, EVENT_CATALOG, FunnelProvider, FunnelToaster, FunnelView, Modal, Next, Script, Sheet, Upsell, VariableStore, attachBus, buildAcquisition, buildContext, createBus, createConsoleTracker, createFunnelStore, createFunnelTree, createMockDriver, createPlatformTracker, defineModal, driverWithEmail, hideModal, integrationEvents, isIntegrationEvent, isTrackableEvent, newEventId, registerModal, removeModal, showModal, toast, unregisterModal, useActiveLocale, useAsset, useAssets, useCheckout, useClickIds, useContextValue, useData, useDevice, useExperiment, useField, useFunnel, useLocale, useModal, useNavigation, useOffering, useOfferings, usePage, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, withBus };
|
|
3081
3685
|
//# sourceMappingURL=index.js.map
|
|
3082
3686
|
//# sourceMappingURL=index.js.map
|