@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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/{checkout-Dz8cGkB_.d.cts → checkout-CMEdxpgo.d.cts} +13 -13
  3. package/dist/{checkout-7Dy6IedP.d.ts → checkout-D1NUPv4p.d.ts} +13 -13
  4. package/dist/{chunk-AKO6XKXP.js → chunk-7YQLPPPG.js} +66 -91
  5. package/dist/chunk-7YQLPPPG.js.map +1 -0
  6. package/dist/{chunk-JSRKA375.cjs → chunk-BPOAWI4G.cjs} +68 -94
  7. package/dist/chunk-BPOAWI4G.cjs.map +1 -0
  8. package/dist/{chunk-M6U3FNRW.js → chunk-CFFMZYPE.js} +3 -3
  9. package/dist/{chunk-M6U3FNRW.js.map → chunk-CFFMZYPE.js.map} +1 -1
  10. package/dist/chunk-EMMSS5I5.cjs +37 -0
  11. package/dist/chunk-EMMSS5I5.cjs.map +1 -0
  12. package/dist/chunk-G3PMV62Z.js +33 -0
  13. package/dist/chunk-G3PMV62Z.js.map +1 -0
  14. package/dist/{chunk-LJYLGLFS.cjs → chunk-QMAZGLGV.cjs} +2 -35
  15. package/dist/chunk-QMAZGLGV.cjs.map +1 -0
  16. package/dist/{chunk-YY375F2B.js → chunk-TNLRQPVY.js} +3 -33
  17. package/dist/chunk-TNLRQPVY.js.map +1 -0
  18. package/dist/{chunk-CY4VBSMX.cjs → chunk-VAOR77NW.cjs} +7 -7
  19. package/dist/{chunk-CY4VBSMX.cjs.map → chunk-VAOR77NW.cjs.map} +1 -1
  20. package/dist/chunk-WYUDL4FI.cjs +8 -0
  21. package/dist/chunk-WYUDL4FI.cjs.map +1 -0
  22. package/dist/chunk-ZZJG4EYL.js +6 -0
  23. package/dist/chunk-ZZJG4EYL.js.map +1 -0
  24. package/dist/driver-paddle.cjs +23 -22
  25. package/dist/driver-paddle.cjs.map +1 -1
  26. package/dist/driver-paddle.d.cts +1 -1
  27. package/dist/driver-paddle.d.ts +1 -1
  28. package/dist/driver-paddle.js +3 -2
  29. package/dist/driver-paddle.js.map +1 -1
  30. package/dist/driver-stripe.cjs +277 -224
  31. package/dist/driver-stripe.cjs.map +1 -1
  32. package/dist/driver-stripe.d.cts +18 -2
  33. package/dist/driver-stripe.d.ts +18 -2
  34. package/dist/driver-stripe.js +244 -192
  35. package/dist/driver-stripe.js.map +1 -1
  36. package/dist/index.cjs +750 -150
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +151 -90
  39. package/dist/index.d.ts +151 -90
  40. package/dist/index.js +702 -98
  41. package/dist/index.js.map +1 -1
  42. package/dist/{manifest-Cr2y1op6.d.cts → manifest-CMe8yVkr.d.cts} +131 -88
  43. package/dist/{manifest-Cr2y1op6.d.ts → manifest-CMe8yVkr.d.ts} +131 -88
  44. package/dist/manifest-entry.cjs +62 -47
  45. package/dist/manifest-entry.cjs.map +1 -1
  46. package/dist/manifest-entry.d.cts +15 -15
  47. package/dist/manifest-entry.d.ts +15 -15
  48. package/dist/manifest-entry.js +25 -14
  49. package/dist/manifest-entry.js.map +1 -1
  50. package/dist/protocol.cjs +13 -0
  51. package/dist/protocol.cjs.map +1 -0
  52. package/dist/protocol.d.cts +182 -0
  53. package/dist/protocol.d.ts +182 -0
  54. package/dist/protocol.js +4 -0
  55. package/dist/protocol.js.map +1 -0
  56. package/package.json +14 -2
  57. package/dist/chunk-AKO6XKXP.js.map +0 -1
  58. package/dist/chunk-JSRKA375.cjs.map +0 -1
  59. package/dist/chunk-LJYLGLFS.cjs.map +0 -1
  60. package/dist/chunk-YY375F2B.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { isRtl, formatProduct, bucketingSeed, resolveExperiments, entryGuard, evaluateGate, expectedPathLength, nextPage, outgoingKeys, buildCatalog, resolveLocale } from './chunk-AKO6XKXP.js';
2
- export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney, formatProduct, hashToUnit, isRtl, isVariantKey, nextPage, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, resolveExperiments, resolveLocale, resolveProduct, resolveRoute, validateExperiments } from './chunk-AKO6XKXP.js';
3
- import { checkoutError, isInlineSurface, PROVIDER_PROFILES } from './chunk-YY375F2B.js';
4
- export { INLINE_SURFACES, PROVIDER_PROFILES, checkoutError, isInlineSurface, isMerchantOfRecord, isOrchestrator, surfacesFor, validateCheckout, validateUpsell } from './chunk-YY375F2B.js';
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
- return { on, off, events, emit, ...accessors };
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 { on, off, getVariable, getVariables, getCurrentPageId, getCustomerId, getVisitorId } = bus;
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 useProduct(id) {
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(() => resolved ? formatProduct(resolved, locale) : void 0, [resolved, locale]);
597
+ return useMemo(
598
+ () => resolved ? formatOffering(resolved, locale) : void 0,
599
+ [resolved, locale]
600
+ );
546
601
  }
547
- function useProducts() {
602
+ function useOfferings() {
548
603
  const catalog = useCatalog();
549
604
  const locale = useActiveLocale();
550
605
  return useMemo(
551
- () => Array.from(catalog.values()).map((r) => formatProduct(r, locale)),
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) => ({ key: p.key, meta: p.meta })),
647
- [flowPages]
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("AppFunnelModalId");
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("No modal dispatch \u2014 render inside <FunnelProvider> (which mounts the modal runtime).");
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) throw new Error("useModal: no modal id (call inside a defineModal component, or pass an id/component).");
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]) registerModal(id, modal, args);
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((a) => showModalVia(d, id, a), [d, id]);
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((v) => {
880
- modalCallbacks[id]?.resolve(v);
881
- delete modalCallbacks[id];
882
- }, [id]);
883
- const reject = useCallback((v) => {
884
- modalCallbacks[id]?.reject(v);
885
- delete modalCallbacks[id];
886
- }, [id]);
887
- const resolveHide = useCallback((v) => {
888
- hideCallbacks[id]?.resolve(v);
889
- delete hideCallbacks[id];
890
- }, [id]);
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
- [id, info?.args, info?.visible, info?.keepMounted, show, hide, remove, resolve, reject, resolveHide]
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({ id, defaultVisible, keepMounted, ...props }) {
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((t) => createElement(t.comp, { key: t.id, id: t.id, ...t.props }))
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 ?? { ok: true, amountMinor: 0, currency: "USD" };
1070
- const offError = typeof options.failOffSession === "object" ? options.failOffSession : checkoutError("authentication_required", "off-session charge declined (mock)");
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) return Promise.resolve({ ok: false, error: offError });
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 cb.onError(success.error ?? checkoutError("processing_error", "failed (mock)"));
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
- const attemptRef = useRef(
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", { amount, currency: result.currency, productId, eventId: result.eventId });
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", { productId, amount, currency, surface, eventId: newEventId() });
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", { productId, amount, currency, eventId: newEventId() });
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(err.category === "authentication_required" ? "requires_action" : "error");
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") void attemptRef.current(product);
1237
- else startRecovery(product, PROVIDER_PROFILES[driver.provider]?.surfaces.sheet ? "sheet" : "popup");
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(route.modal, props);
1586
+ void showModal(
1587
+ route.modal,
1588
+ props
1589
+ );
1244
1590
  } else if ("fallback" in route) {
1245
- if (product && !recovering.current) startRecovery(product, route.fallback);
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({ product, intent, surface, kind });
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 failRef.current(result.error ?? checkoutError("unknown", "Checkout failed"), product);
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("processing_error", e instanceof Error ? e.message : "Checkout failed");
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(async (product, surface) => {
1262
- recovering.current = false;
1263
- return attemptRef.current(product, surface);
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 { open, status, error, isLoading: status === "loading", reset, callbacksFor };
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("processing_error", "This checkout driver has no inline surface to host in the sheet")
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(checkoutError("canceled", "Checkout was dismissed before completing"));
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, { asChild, children, className, disabled }) {
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("button", { type: "button", className, disabled, onClick }, children ?? "Continue");
1717
+ return createElement(
1718
+ "button",
1719
+ { type: "button", className, disabled, onClick },
1720
+ children ?? "Continue"
1721
+ );
1336
1722
  }
1337
1723
  function Checkout({
1338
- product,
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({ product, intent, surface }, checkout.callbacksFor(product, surface))
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, { asChild, children, className, disabled: checkout.isLoading });
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({ product, fallback, asChild, children, className, ...options }) {
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(() => void checkout.open(product), {
1385
- asChild,
1386
- children: children ?? "Add to my plan",
1387
- className,
1388
- disabled: checkout.isLoading
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(window.history.state, "", url.toString());
1813
+ window.history.replaceState(
1814
+ window.history.state,
1815
+ "",
1816
+ url.toString()
1817
+ );
1408
1818
  cb.onSuccess(result);
1409
1819
  } else {
1410
- cb.onError(result.error ?? checkoutError("unknown", "The checkout could not be verified"));
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)) out[key.slice(this.dot.length)] = all[key];
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
- products,
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
- sessionValues
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(products ?? []);
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(driverWithEmail(checkout ?? createMockDriver(), emailForCheckout), catalog),
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("useFunnel/useResponse/\u2026 must be used inside <FunnelProvider>");
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(`useField: "${path}" is not a writable namespace (user/responses/data)`);
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((acc, k) => acc?.[k], ctx),
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 doPost = () => {
2830
- if (!pendingVars || !sessionId) return;
2831
- const userData = pendingVars;
2832
- pendingVars = null;
2833
- void fetch(`${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event/session/data`, {
2834
- method: "POST",
2835
- headers: { "content-type": "application/json" },
2836
- body: JSON.stringify({ sessionId, userData }),
2837
- keepalive: true
2838
- }).catch((e) => {
2839
- if (process.env.NODE_ENV !== "production") console.warn("[appfunnel] variable persist failed:", e);
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("pagehide", flushVars);
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
- products: opts.products ?? [],
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, usePage, useProduct, useProducts, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, withBus };
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