@appfunnel-dev/sdk 2.0.0-canary.4 → 2.0.0-canary.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-AKO6XKXP.js → chunk-AXXG3ODP.js} +10 -3
- package/dist/chunk-AXXG3ODP.js.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-JSRKA375.cjs → chunk-G44BUG6G.cjs} +10 -3
- package/dist/chunk-G44BUG6G.cjs.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.js +3 -2
- package/dist/driver-paddle.js.map +1 -1
- package/dist/driver-stripe.cjs +37 -36
- package/dist/driver-stripe.cjs.map +1 -1
- package/dist/driver-stripe.js +3 -2
- package/dist/driver-stripe.js.map +1 -1
- package/dist/index.cjs +504 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -74
- package/dist/index.d.ts +135 -74
- package/dist/index.js +447 -23
- package/dist/index.js.map +1 -1
- package/dist/{manifest-Cr2y1op6.d.cts → manifest-Dl7faaKe.d.cts} +34 -0
- package/dist/{manifest-Cr2y1op6.d.ts → manifest-Dl7faaKe.d.ts} +34 -0
- package/dist/manifest-entry.cjs +45 -30
- package/dist/manifest-entry.cjs.map +1 -1
- package/dist/manifest-entry.d.cts +1 -1
- package/dist/manifest-entry.d.ts +1 -1
- package/dist/manifest-entry.js +10 -3
- 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, formatProduct, bucketingSeed, resolveExperiments, entryGuard, evaluateGate, expectedPathLength, nextPage, outgoingKeys, buildCatalog, resolveLocale } from './chunk-
|
|
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-
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { isRtl, formatProduct, bucketingSeed, resolveExperiments, entryGuard, evaluateGate, expectedPathLength, nextPage, outgoingKeys, buildCatalog, resolveLocale } from './chunk-AXXG3ODP.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-AXXG3ODP.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,27 @@ 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("[appfunnel] window.appfunnel navigation called before the funnel flow mounted \u2014 ignored");
|
|
391
|
+
}
|
|
392
|
+
return nav;
|
|
393
|
+
};
|
|
394
|
+
const registerNavigation = (n) => {
|
|
395
|
+
nav = n;
|
|
396
|
+
warnedNoNav = false;
|
|
397
|
+
return () => {
|
|
398
|
+
if (nav === n) nav = null;
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
const goTo = (pageKey) => currentNav()?.goTo(pageKey) ?? false;
|
|
402
|
+
const goNext = () => currentNav()?.goNext();
|
|
403
|
+
const goPrevious = () => currentNav()?.goPrevious();
|
|
404
|
+
const navigation = () => nav?.snapshot() ?? null;
|
|
405
|
+
return { on, off, events, emit, registerNavigation, goTo, goNext, goPrevious, navigation, ...accessors };
|
|
384
406
|
}
|
|
385
407
|
function withBus(tracker, bus) {
|
|
386
408
|
return {
|
|
@@ -402,7 +424,19 @@ function withBus(tracker, bus) {
|
|
|
402
424
|
function attachBus(bus) {
|
|
403
425
|
if (typeof window === "undefined") return () => {
|
|
404
426
|
};
|
|
405
|
-
const {
|
|
427
|
+
const {
|
|
428
|
+
on,
|
|
429
|
+
off,
|
|
430
|
+
getVariable,
|
|
431
|
+
getVariables,
|
|
432
|
+
getCurrentPageId,
|
|
433
|
+
getCustomerId,
|
|
434
|
+
getVisitorId,
|
|
435
|
+
goTo,
|
|
436
|
+
goNext,
|
|
437
|
+
goPrevious,
|
|
438
|
+
navigation
|
|
439
|
+
} = bus;
|
|
406
440
|
const api = {
|
|
407
441
|
on,
|
|
408
442
|
off,
|
|
@@ -411,6 +445,11 @@ function attachBus(bus) {
|
|
|
411
445
|
getCurrentPageId,
|
|
412
446
|
getCustomerId,
|
|
413
447
|
getVisitorId,
|
|
448
|
+
// Flow control (bound closures over the bus's live nav registration).
|
|
449
|
+
goTo,
|
|
450
|
+
goNext,
|
|
451
|
+
goPrevious,
|
|
452
|
+
navigation,
|
|
414
453
|
get events() {
|
|
415
454
|
return bus.events;
|
|
416
455
|
}
|
|
@@ -552,6 +591,102 @@ function useProducts() {
|
|
|
552
591
|
[catalog, locale]
|
|
553
592
|
);
|
|
554
593
|
}
|
|
594
|
+
|
|
595
|
+
// src/flow/screenshot.ts
|
|
596
|
+
var MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
597
|
+
var ScreenshotError = class extends Error {
|
|
598
|
+
constructor(code, message) {
|
|
599
|
+
super(message);
|
|
600
|
+
this.code = code;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
function resolveTarget(t) {
|
|
604
|
+
if (!t || t.kind === "page") return document.getElementById("root") ?? document.body;
|
|
605
|
+
if (t.kind === "viewport") return document.body;
|
|
606
|
+
return document.querySelector(t.selector);
|
|
607
|
+
}
|
|
608
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
609
|
+
function hideAll(selectors) {
|
|
610
|
+
const touched = [];
|
|
611
|
+
for (const sel of selectors) {
|
|
612
|
+
document.querySelectorAll(sel).forEach((el) => {
|
|
613
|
+
touched.push([el, el.style.visibility]);
|
|
614
|
+
el.style.visibility = "hidden";
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return () => touched.forEach(([el, v]) => el.style.visibility = v);
|
|
618
|
+
}
|
|
619
|
+
async function scrollThrough(el) {
|
|
620
|
+
const prev = el.scrollTop;
|
|
621
|
+
const step = el.clientHeight || window.innerHeight;
|
|
622
|
+
for (let y = 0; y < el.scrollHeight; y += step) {
|
|
623
|
+
el.scrollTop = y;
|
|
624
|
+
await delay(16);
|
|
625
|
+
}
|
|
626
|
+
el.scrollTop = prev;
|
|
627
|
+
await delay(16);
|
|
628
|
+
}
|
|
629
|
+
function blobToDataURL(blob) {
|
|
630
|
+
return new Promise((resolve, reject) => {
|
|
631
|
+
const fr = new FileReader();
|
|
632
|
+
fr.onload = () => resolve(String(fr.result));
|
|
633
|
+
fr.onerror = () => reject(fr.error);
|
|
634
|
+
fr.readAsDataURL(blob);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
async function captureScreenshot(params = {}) {
|
|
638
|
+
const el = resolveTarget(params.target);
|
|
639
|
+
if (!el) throw new ScreenshotError("bad_params", "screenshot target not found");
|
|
640
|
+
const restore = params.hideSelectors?.length ? hideAll(params.hideSelectors) : null;
|
|
641
|
+
const scale = Math.min(params.scale ?? 2, params.maxScale ?? 3);
|
|
642
|
+
const format = params.format ?? "png";
|
|
643
|
+
const timeout = params.timeout ?? 1e4;
|
|
644
|
+
try {
|
|
645
|
+
if (params.waitForFonts !== false && document.fonts?.ready) {
|
|
646
|
+
await Promise.race([document.fonts.ready, delay(timeout)]);
|
|
647
|
+
}
|
|
648
|
+
if (params.triggerLazyLoad) await scrollThrough(el);
|
|
649
|
+
if (params.waitForImages !== false) {
|
|
650
|
+
await Promise.all(
|
|
651
|
+
Array.from(el.querySelectorAll("img")).map(
|
|
652
|
+
(img) => typeof img.decode === "function" ? img.decode().catch(() => {
|
|
653
|
+
}) : Promise.resolve()
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
const { domToBlob } = await import('modern-screenshot');
|
|
658
|
+
let blob;
|
|
659
|
+
try {
|
|
660
|
+
blob = await domToBlob(el, {
|
|
661
|
+
type: MIME[format],
|
|
662
|
+
quality: params.quality ?? 0.92,
|
|
663
|
+
scale,
|
|
664
|
+
width: params.width,
|
|
665
|
+
height: params.height,
|
|
666
|
+
// `null` = transparent (png/webp); only fall back to white when the caller omitted it. `??`
|
|
667
|
+
// would wrongly collapse an explicit `null` to white.
|
|
668
|
+
backgroundColor: params.backgroundColor === void 0 ? "#ffffff" : params.backgroundColor,
|
|
669
|
+
timeout
|
|
670
|
+
});
|
|
671
|
+
} catch (e) {
|
|
672
|
+
throw new ScreenshotError("failed", `capture failed (an image/font may be served without CORS '*'): ${String(e?.message || e)}`);
|
|
673
|
+
}
|
|
674
|
+
const result = {
|
|
675
|
+
mime: MIME[format],
|
|
676
|
+
width: Math.round((params.width ?? el.clientWidth) * scale),
|
|
677
|
+
height: Math.round((params.height ?? el.scrollHeight) * scale),
|
|
678
|
+
scale,
|
|
679
|
+
bytesApprox: blob.size
|
|
680
|
+
};
|
|
681
|
+
if ((params.encoding ?? "blob") === "dataURL") result.dataURL = await blobToDataURL(blob);
|
|
682
|
+
else result.blob = blob;
|
|
683
|
+
return result;
|
|
684
|
+
} finally {
|
|
685
|
+
restore?.();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/flow/flow.tsx
|
|
555
690
|
var NavContext = createContext(null);
|
|
556
691
|
var ExperimentContext = createContext({});
|
|
557
692
|
function useExperiment(id) {
|
|
@@ -604,6 +739,7 @@ function usePageExitTimer(tracker, currentKey) {
|
|
|
604
739
|
function FunnelView({
|
|
605
740
|
pages,
|
|
606
741
|
initialKey,
|
|
742
|
+
trustInitialKey = false,
|
|
607
743
|
layout,
|
|
608
744
|
fallback,
|
|
609
745
|
prefetch: prefetchMode = "auto",
|
|
@@ -651,6 +787,7 @@ function FunnelView({
|
|
|
651
787
|
if (!initialKey || initialKey === start) return start ? [start] : [];
|
|
652
788
|
const target = toSlot(initialKey);
|
|
653
789
|
const page2 = flowPages.find((p) => p.key === target);
|
|
790
|
+
if (trustInitialKey) return page2 ? [target] : start ? [start] : [];
|
|
654
791
|
const guard = entryGuard(page2?.meta);
|
|
655
792
|
const ok = page2 && (!guard || evaluateGate(guard, funnel.snapshot()));
|
|
656
793
|
const first = ok ? target : start;
|
|
@@ -736,6 +873,44 @@ function FunnelView({
|
|
|
736
873
|
canGoBack: history.length > 1,
|
|
737
874
|
isNavigating
|
|
738
875
|
};
|
|
876
|
+
const bus = useBusInternal();
|
|
877
|
+
const navRef = useRef(value);
|
|
878
|
+
navRef.current = value;
|
|
879
|
+
useEffect(() => {
|
|
880
|
+
if (!bus) return;
|
|
881
|
+
const control = {
|
|
882
|
+
goTo: (key) => {
|
|
883
|
+
if (!components.has(toRenderKey(key))) return false;
|
|
884
|
+
navRef.current.go(key);
|
|
885
|
+
return true;
|
|
886
|
+
},
|
|
887
|
+
goNext: () => navRef.current.next(),
|
|
888
|
+
goPrevious: () => navRef.current.back(),
|
|
889
|
+
snapshot: () => {
|
|
890
|
+
const v = navRef.current;
|
|
891
|
+
return {
|
|
892
|
+
key: v.page.key,
|
|
893
|
+
index: v.page.index,
|
|
894
|
+
total: v.page.total,
|
|
895
|
+
progressPercentage: v.page.progressPercentage,
|
|
896
|
+
canGoBack: v.canGoBack,
|
|
897
|
+
nextCandidates: v.nextCandidates
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
return bus.registerNavigation(control);
|
|
902
|
+
}, [bus]);
|
|
903
|
+
useEffect(() => {
|
|
904
|
+
const v = navRef.current;
|
|
905
|
+
bus?.emit("navigation", {
|
|
906
|
+
key: v.page.key,
|
|
907
|
+
index: v.page.index,
|
|
908
|
+
total: v.page.total,
|
|
909
|
+
progressPercentage: v.page.progressPercentage,
|
|
910
|
+
canGoBack: v.canGoBack,
|
|
911
|
+
nextCandidates: v.nextCandidates
|
|
912
|
+
});
|
|
913
|
+
}, [bus, currentKey]);
|
|
739
914
|
const renderKey = toRenderKey(currentKey);
|
|
740
915
|
const Current = components.get(renderKey);
|
|
741
916
|
const pageEl = createElement(Suspense, { fallback: fallback ?? null }, Current ? createElement(Current) : null);
|
|
@@ -751,6 +926,81 @@ function useNavigation() {
|
|
|
751
926
|
if (!ctx) throw new Error("useNavigation must be used inside <FunnelView>");
|
|
752
927
|
return ctx;
|
|
753
928
|
}
|
|
929
|
+
var EMBED_CAPABILITIES = {
|
|
930
|
+
methods: ["nav.snapshot", "nav.goTo", "nav.goNext", "nav.goPrevious", "screenshot"],
|
|
931
|
+
events: ["navigation"]
|
|
932
|
+
};
|
|
933
|
+
function EmbedBridge() {
|
|
934
|
+
const bus = useBusInternal();
|
|
935
|
+
useEffect(() => {
|
|
936
|
+
if (!bus || typeof window === "undefined" || window.parent === window) return;
|
|
937
|
+
const parent = window.parent;
|
|
938
|
+
let disposed = false;
|
|
939
|
+
const send = (msg) => {
|
|
940
|
+
try {
|
|
941
|
+
parent.postMessage({ af: AF_PROTOCOL, source: "af-preview", ...msg }, "*");
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const hello = () => send({ kind: "hello", role: "funnel", protocol: AF_PROTOCOL, capabilities: EMBED_CAPABILITIES });
|
|
946
|
+
const sendSnapshot = () => {
|
|
947
|
+
const s = bus.navigation();
|
|
948
|
+
if (s) send({ kind: "event", event: "navigation", data: s });
|
|
949
|
+
};
|
|
950
|
+
async function invoke(method, params) {
|
|
951
|
+
switch (method) {
|
|
952
|
+
case "nav.snapshot":
|
|
953
|
+
return bus.navigation();
|
|
954
|
+
case "nav.goTo": {
|
|
955
|
+
const key = params?.key;
|
|
956
|
+
if (typeof key !== "string" || !bus.goTo(key)) throw { code: "bad_params", message: `unknown page key: ${String(key)}` };
|
|
957
|
+
return void 0;
|
|
958
|
+
}
|
|
959
|
+
case "nav.goNext":
|
|
960
|
+
bus.goNext();
|
|
961
|
+
return void 0;
|
|
962
|
+
case "nav.goPrevious":
|
|
963
|
+
bus.goPrevious();
|
|
964
|
+
return void 0;
|
|
965
|
+
case "screenshot":
|
|
966
|
+
return await captureScreenshot(params ?? {});
|
|
967
|
+
default:
|
|
968
|
+
throw { code: "unknown_method", message: `unknown method: ${method}` };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function onMsg(e) {
|
|
972
|
+
if (e.source !== parent) return;
|
|
973
|
+
const d = e.data;
|
|
974
|
+
if (!d || typeof d !== "object" || d.af !== AF_PROTOCOL || d.source !== "af-host") return;
|
|
975
|
+
if (d.kind === "hello") {
|
|
976
|
+
hello();
|
|
977
|
+
sendSnapshot();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (d.kind !== "request" || typeof d.id !== "string") return;
|
|
981
|
+
try {
|
|
982
|
+
const result = await invoke(d.method, d.params);
|
|
983
|
+
if (!disposed) send({ kind: "response", id: d.id, ok: true, result });
|
|
984
|
+
} catch (err) {
|
|
985
|
+
const e2 = err;
|
|
986
|
+
const error = e2 && typeof e2.code === "string" ? { code: e2.code, message: String(e2.message ?? e2.code) } : { code: "failed", message: String(e2?.message ?? err) };
|
|
987
|
+
if (!disposed) send({ kind: "response", id: d.id, ok: false, error });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
window.addEventListener("message", onMsg);
|
|
991
|
+
const offNav = bus.on("navigation", (data) => {
|
|
992
|
+
if (!disposed) send({ kind: "event", event: "navigation", data });
|
|
993
|
+
});
|
|
994
|
+
hello();
|
|
995
|
+
sendSnapshot();
|
|
996
|
+
return () => {
|
|
997
|
+
disposed = true;
|
|
998
|
+
offNav();
|
|
999
|
+
window.removeEventListener("message", onMsg);
|
|
1000
|
+
};
|
|
1001
|
+
}, [bus]);
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
754
1004
|
function usePage() {
|
|
755
1005
|
return useNavigation().page;
|
|
756
1006
|
}
|
|
@@ -2568,6 +2818,7 @@ var FunnelCtx = createContext(null);
|
|
|
2568
2818
|
function FunnelProvider({
|
|
2569
2819
|
config,
|
|
2570
2820
|
sessionValues,
|
|
2821
|
+
deferSessionValues = false,
|
|
2571
2822
|
tracker,
|
|
2572
2823
|
context: contextOpts,
|
|
2573
2824
|
products,
|
|
@@ -2583,7 +2834,12 @@ function FunnelProvider({
|
|
|
2583
2834
|
const [value] = useState(() => {
|
|
2584
2835
|
const s = store ?? createFunnelStore(
|
|
2585
2836
|
{ responses: config?.responses, data: config?.data },
|
|
2586
|
-
|
|
2837
|
+
// FLASH-FREE HYDRATION: with deferSessionValues the initial render is seeded with
|
|
2838
|
+
// DEFAULTS ONLY, so the client's first render is byte-identical to the build-time
|
|
2839
|
+
// DEFAULT-state prerender it hydrates against (clean match). The visitor's saved
|
|
2840
|
+
// answers arrive as a post-hydration setMany below. Without the flag, sessionValues
|
|
2841
|
+
// seed the store at construction (SSR + non-prerendered paths).
|
|
2842
|
+
deferSessionValues ? void 0 : sessionValues
|
|
2587
2843
|
);
|
|
2588
2844
|
const ctx = buildContext({
|
|
2589
2845
|
funnelId: config?.id,
|
|
@@ -2615,6 +2871,12 @@ function FunnelProvider({
|
|
|
2615
2871
|
};
|
|
2616
2872
|
});
|
|
2617
2873
|
useEffect(() => attachBus(value.bus), [value.bus]);
|
|
2874
|
+
useEffect(() => {
|
|
2875
|
+
if (!deferSessionValues || !sessionValues) return;
|
|
2876
|
+
const updates = {};
|
|
2877
|
+
for (const [key, v] of Object.entries(sessionValues)) if (v !== void 0) updates[key] = v;
|
|
2878
|
+
if (Object.keys(updates).length > 0) value.store.setMany(updates);
|
|
2879
|
+
}, []);
|
|
2618
2880
|
useEffect(() => {
|
|
2619
2881
|
value.tracker.setAcquisition?.(buildAcquisition(value.context));
|
|
2620
2882
|
}, [value]);
|
|
@@ -2650,6 +2912,9 @@ function useFunnelCtx() {
|
|
|
2650
2912
|
function useFunnel() {
|
|
2651
2913
|
return useFunnelCtx().funnel;
|
|
2652
2914
|
}
|
|
2915
|
+
function useBusInternal() {
|
|
2916
|
+
return useContext(FunnelCtx)?.bus ?? null;
|
|
2917
|
+
}
|
|
2653
2918
|
function useStoreKey(store, key) {
|
|
2654
2919
|
const subscribe = useCallback(
|
|
2655
2920
|
(cb) => store.subscribe(cb, { keys: [key] }),
|
|
@@ -2713,6 +2978,152 @@ function useContextValue(path) {
|
|
|
2713
2978
|
);
|
|
2714
2979
|
}
|
|
2715
2980
|
|
|
2981
|
+
// src/tracking/snapshotPersistence.ts
|
|
2982
|
+
function createSnapshotPersistence(opts) {
|
|
2983
|
+
const baseDelay = opts.baseDelayMs ?? 1e3;
|
|
2984
|
+
const maxDelay = opts.maxDelayMs ?? 3e4;
|
|
2985
|
+
const setT = opts.timers?.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
2986
|
+
const clearT = opts.timers?.clearTimeout ?? ((h) => clearTimeout(h));
|
|
2987
|
+
const rand = opts.random ?? Math.random;
|
|
2988
|
+
let pending = null;
|
|
2989
|
+
let seq = 0;
|
|
2990
|
+
let pendingSeq = 0;
|
|
2991
|
+
let inFlight = false;
|
|
2992
|
+
let attempt = 0;
|
|
2993
|
+
let retryTimer = null;
|
|
2994
|
+
let disposed = false;
|
|
2995
|
+
let fatalSeq = -1;
|
|
2996
|
+
const clearRetry = () => {
|
|
2997
|
+
if (retryTimer !== null) {
|
|
2998
|
+
clearT(retryTimer);
|
|
2999
|
+
retryTimer = null;
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
const buildBody = (snapshot, s) => JSON.stringify({ sessionId: opts.getSessionId(), userData: snapshot, seq: s });
|
|
3003
|
+
const trySend = () => {
|
|
3004
|
+
if (disposed || inFlight || pending === null) return;
|
|
3005
|
+
if (retryTimer !== null) return;
|
|
3006
|
+
const sessionId = opts.getSessionId();
|
|
3007
|
+
if (!sessionId) return;
|
|
3008
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return;
|
|
3009
|
+
if (pendingSeq === fatalSeq) return;
|
|
3010
|
+
const snapshot = pending;
|
|
3011
|
+
const sentSeq = pendingSeq;
|
|
3012
|
+
const body = buildBody(snapshot, sentSeq);
|
|
3013
|
+
inFlight = true;
|
|
3014
|
+
void fetch(opts.url, {
|
|
3015
|
+
method: "POST",
|
|
3016
|
+
headers: { "content-type": "application/json" },
|
|
3017
|
+
body,
|
|
3018
|
+
keepalive: true
|
|
3019
|
+
}).then((res) => {
|
|
3020
|
+
inFlight = false;
|
|
3021
|
+
if (disposed) return;
|
|
3022
|
+
if (res.ok) {
|
|
3023
|
+
attempt = 0;
|
|
3024
|
+
if (pendingSeq === sentSeq) pending = null;
|
|
3025
|
+
else trySend();
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
if (res.status >= 400 && res.status < 500) {
|
|
3029
|
+
if (process.env.NODE_ENV !== "production") {
|
|
3030
|
+
console.warn(
|
|
3031
|
+
`[appfunnel] snapshot persist rejected ${res.status} (dropping snapshot; check session/payload)`
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
fatalSeq = sentSeq;
|
|
3035
|
+
if (pendingSeq === sentSeq) pending = null;
|
|
3036
|
+
else trySend();
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
scheduleRetry();
|
|
3040
|
+
}).catch(() => {
|
|
3041
|
+
inFlight = false;
|
|
3042
|
+
if (disposed) return;
|
|
3043
|
+
scheduleRetry();
|
|
3044
|
+
});
|
|
3045
|
+
};
|
|
3046
|
+
const scheduleRetry = () => {
|
|
3047
|
+
if (disposed || pending === null) return;
|
|
3048
|
+
const exp = Math.min(maxDelay, baseDelay * 2 ** attempt);
|
|
3049
|
+
const delay2 = Math.round(exp * (0.5 + rand() * 0.5));
|
|
3050
|
+
attempt++;
|
|
3051
|
+
clearRetry();
|
|
3052
|
+
retryTimer = setT(() => {
|
|
3053
|
+
retryTimer = null;
|
|
3054
|
+
trySend();
|
|
3055
|
+
}, delay2);
|
|
3056
|
+
};
|
|
3057
|
+
const push = (snapshot) => {
|
|
3058
|
+
if (disposed) return;
|
|
3059
|
+
pending = snapshot;
|
|
3060
|
+
pendingSeq = ++seq;
|
|
3061
|
+
if (fatalSeq !== -1 && pendingSeq !== fatalSeq) fatalSeq = -1;
|
|
3062
|
+
if (retryTimer === null) trySend();
|
|
3063
|
+
};
|
|
3064
|
+
const flushBeacon = () => {
|
|
3065
|
+
if (disposed || pending === null) return;
|
|
3066
|
+
const sessionId = opts.getSessionId();
|
|
3067
|
+
if (!sessionId) return;
|
|
3068
|
+
const body = buildBody(pending, pendingSeq);
|
|
3069
|
+
let delivered = false;
|
|
3070
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
3071
|
+
try {
|
|
3072
|
+
delivered = navigator.sendBeacon(
|
|
3073
|
+
opts.url,
|
|
3074
|
+
new Blob([body], { type: "application/json" })
|
|
3075
|
+
);
|
|
3076
|
+
} catch {
|
|
3077
|
+
delivered = false;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
if (!delivered) {
|
|
3081
|
+
try {
|
|
3082
|
+
void fetch(opts.url, {
|
|
3083
|
+
method: "POST",
|
|
3084
|
+
headers: { "content-type": "application/json" },
|
|
3085
|
+
body,
|
|
3086
|
+
keepalive: true
|
|
3087
|
+
}).catch(() => {
|
|
3088
|
+
});
|
|
3089
|
+
delivered = true;
|
|
3090
|
+
} catch {
|
|
3091
|
+
delivered = false;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
if (delivered) pending = null;
|
|
3095
|
+
};
|
|
3096
|
+
const onHidden = () => {
|
|
3097
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
3098
|
+
flushBeacon();
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
const onPageHide = () => flushBeacon();
|
|
3102
|
+
const onOnline = () => trySend();
|
|
3103
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
3104
|
+
try {
|
|
3105
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
3106
|
+
window.addEventListener("pagehide", onPageHide);
|
|
3107
|
+
window.addEventListener("online", onOnline);
|
|
3108
|
+
} catch {
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
const dispose = () => {
|
|
3112
|
+
disposed = true;
|
|
3113
|
+
clearRetry();
|
|
3114
|
+
pending = null;
|
|
3115
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
3116
|
+
try {
|
|
3117
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
3118
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
3119
|
+
window.removeEventListener("online", onOnline);
|
|
3120
|
+
} catch {
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
return { push, flushBeacon, dispose };
|
|
3125
|
+
}
|
|
3126
|
+
|
|
2716
3127
|
// src/tracking/platformTracker.ts
|
|
2717
3128
|
function validClickIds(acq) {
|
|
2718
3129
|
return Object.entries(acq.clickIds ?? {}).filter(
|
|
@@ -2818,32 +3229,35 @@ function createPlatformTracker(cfg) {
|
|
|
2818
3229
|
}
|
|
2819
3230
|
};
|
|
2820
3231
|
const VARS_DEBOUNCE_MS = 800;
|
|
3232
|
+
const dataUrl = `${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event/session/data`;
|
|
2821
3233
|
let pendingVars = null;
|
|
2822
3234
|
let varsTimer = null;
|
|
3235
|
+
const persistence = createSnapshotPersistence({
|
|
3236
|
+
url: dataUrl,
|
|
3237
|
+
getSessionId: () => sessionId
|
|
3238
|
+
});
|
|
2823
3239
|
const flushVars = () => {
|
|
2824
3240
|
if (varsTimer) {
|
|
2825
3241
|
clearTimeout(varsTimer);
|
|
2826
3242
|
varsTimer = null;
|
|
2827
3243
|
}
|
|
2828
3244
|
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);
|
|
3245
|
+
const snapshot = pendingVars;
|
|
3246
|
+
pendingVars = null;
|
|
3247
|
+
persistence.push(snapshot);
|
|
3248
|
+
if (!sessionId) chain = chain.then(() => {
|
|
3249
|
+
persistence.push(snapshot);
|
|
3250
|
+
});
|
|
3251
|
+
};
|
|
3252
|
+
const drainOnExit = () => {
|
|
3253
|
+
if (pendingVars) {
|
|
3254
|
+
flushVars();
|
|
3255
|
+
persistence.flushBeacon();
|
|
3256
|
+
}
|
|
2844
3257
|
};
|
|
2845
3258
|
try {
|
|
2846
|
-
window.addEventListener("
|
|
3259
|
+
window.addEventListener("visibilitychange", drainOnExit);
|
|
3260
|
+
window.addEventListener("pagehide", drainOnExit);
|
|
2847
3261
|
} catch {
|
|
2848
3262
|
}
|
|
2849
3263
|
if (cfg.experiment?.id && cfg.experiment.variant) {
|
|
@@ -2886,6 +3300,7 @@ function createFunnelTree({
|
|
|
2886
3300
|
}) {
|
|
2887
3301
|
return function tree(opts) {
|
|
2888
3302
|
const mode = opts.mode ?? "live";
|
|
3303
|
+
const embedEnabled = opts.embed ?? false;
|
|
2889
3304
|
const tracker = opts.tracking ? createPlatformTracker({
|
|
2890
3305
|
...opts.tracking,
|
|
2891
3306
|
visitorId: opts.visitorId,
|
|
@@ -2907,6 +3322,7 @@ function createFunnelTree({
|
|
|
2907
3322
|
Fragment,
|
|
2908
3323
|
null,
|
|
2909
3324
|
createElement(CheckoutResume, null),
|
|
3325
|
+
embedEnabled ? createElement(EmbedBridge, null) : null,
|
|
2910
3326
|
layout ? createElement(layout, null, children) : children
|
|
2911
3327
|
);
|
|
2912
3328
|
};
|
|
@@ -2920,6 +3336,7 @@ function createFunnelTree({
|
|
|
2920
3336
|
tracker,
|
|
2921
3337
|
checkout,
|
|
2922
3338
|
sessionValues: opts.sessionValues,
|
|
3339
|
+
deferSessionValues: opts.deferSessionValues,
|
|
2923
3340
|
experiments: opts.experiments,
|
|
2924
3341
|
context: {
|
|
2925
3342
|
mode,
|
|
@@ -2931,6 +3348,13 @@ function createFunnelTree({
|
|
|
2931
3348
|
pages,
|
|
2932
3349
|
layout: layoutWithResume,
|
|
2933
3350
|
initialKey: opts.initialKey,
|
|
3351
|
+
// FLASH-FREE HYDRATION (Option B, FIX 1): trust the SERVER-resolved initialKey instead of
|
|
3352
|
+
// re-running the entry guard. The CLIENT sets this via deferSessionValues (its deferred
|
|
3353
|
+
// snapshot is empty, so re-running the guard would bounce a qualified deep-link and
|
|
3354
|
+
// mismatch the markup). The build-time PRERENDER sets `trustInitialKey` explicitly so a
|
|
3355
|
+
// guarded page prerenders its OWN default-state markup (the renderer picks target-vs-bounce
|
|
3356
|
+
// at serve time). Live SSR fallback leaves both off → the guard runs against real values.
|
|
3357
|
+
trustInitialKey: opts.trustInitialKey ?? opts.deferSessionValues,
|
|
2934
3358
|
prefetch: "off"
|
|
2935
3359
|
// renderer background-modulepreloads all chunks
|
|
2936
3360
|
})
|