@funnelsgrove/runtime 0.1.56 → 0.1.60
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/components/FunnelContext.d.ts +5 -1
- package/dist/components/FunnelContext.js +2 -1
- package/dist/components/ManageSubscriptionScreen.js +4 -1
- package/dist/components/SubscriptionHandoffScreen.d.ts +2 -1
- package/dist/components/SubscriptionHandoffScreen.js +11 -6
- package/dist/config/funnel.manifest.types.d.ts +4 -2
- package/dist/generated/step-contract-hash.d.ts +1 -0
- package/dist/generated/step-contract-hash.js +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/migrations/step-contract-v2.d.ts +50 -0
- package/dist/migrations/step-contract-v2.js +310 -0
- package/dist/migrations/step-contract-v3.d.ts +41 -0
- package/dist/migrations/step-contract-v3.js +26 -0
- package/dist/runtime/funnel-manifest.validation.d.ts +7 -2
- package/dist/runtime/funnel-manifest.validation.js +24 -68
- package/dist/runtime/funnel-step-lifecycle.d.ts +35 -0
- package/dist/runtime/funnel-step-lifecycle.js +49 -0
- package/dist/runtime/funnel-step-metadata.validation.d.ts +7 -0
- package/dist/runtime/funnel-step-metadata.validation.js +88 -0
- package/dist/runtime/offer-set-runtime.d.ts +17 -0
- package/dist/runtime/offer-set-runtime.js +18 -6
- package/dist/runtime/submit-email-capture.d.ts +23 -0
- package/dist/runtime/submit-email-capture.js +54 -0
- package/dist/runtime/use-funnel-flow-controller.d.ts +58 -5
- package/dist/runtime/use-funnel-flow-controller.js +804 -122
- package/dist/runtime/use-step-choices.d.ts +58 -0
- package/dist/runtime/use-step-choices.js +187 -0
- package/dist/sdk/userAnswers.d.ts +1 -0
- package/dist/services/api.service.d.ts +10 -0
- package/dist/services/api.service.js +32 -0
- package/dist/services/funnel-sdk.service.d.ts +27 -0
- package/dist/services/funnel-sdk.service.js +18 -0
- package/dist/steps/choice-contract.d.ts +33 -0
- package/dist/steps/choice-contract.js +302 -0
- package/dist/steps/step-contract.d.ts +498 -0
- package/dist/steps/step-contract.js +506 -0
- package/dist/steps/types.d.ts +7 -5
- package/dist/steps/types.js +1 -18
- package/dist/testing/funnel-contract-journey-driver.d.ts +35 -0
- package/dist/testing/funnel-contract-journey-driver.js +266 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/validation/fixtures/invalid-reserved-identities.d.ts +249 -0
- package/dist/validation/fixtures/invalid-reserved-identities.js +20 -0
- package/dist/validation/fixtures/valid-v2-project.d.ts +419 -0
- package/dist/validation/fixtures/valid-v2-project.js +228 -0
- package/dist/validation/fixtures/valid-v3-project.d.ts +419 -0
- package/dist/validation/fixtures/valid-v3-project.js +30 -0
- package/dist/validation/funnel-contract-diagnostics.d.ts +49 -0
- package/dist/validation/funnel-contract-diagnostics.js +67 -0
- package/dist/validation/funnel-contract-validator.d.ts +30 -0
- package/dist/validation/funnel-contract-validator.js +1458 -0
- package/dist/validation/funnel-manifest-input.normalization.d.ts +67 -0
- package/dist/validation/funnel-manifest-input.normalization.js +386 -0
- package/package.json +9 -1
|
@@ -13,6 +13,16 @@ import { resolveUrlUserAttributes } from './url-user-attributes.js';
|
|
|
13
13
|
import { bootstrapPostHog, identifyPostHog, isPostHogReady, resolveExperimentVariant, } from './posthog-flags.js';
|
|
14
14
|
import { getRuntimeMode, isEditorEnabled, useRuntimeMode } from '../services/runtime-mode.service.js';
|
|
15
15
|
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
16
|
+
import { FUNNEL_STEP_CONTRACTS, LEGACY_UNVERSIONED_STEP_CONTRACT_VERSION, } from '../steps/step-contract.js';
|
|
17
|
+
import { isDevelopmentRuntime } from '../config/env.config.js';
|
|
18
|
+
import { consumeChoiceCompletionMarker, enforceChoiceBypassGuard, registerInternalChoiceCommit, runAtomicChoiceCommit, } from './use-step-choices.js';
|
|
19
|
+
import { createFunnelStepLifecycleState, finishVisit, startVisit, } from './funnel-step-lifecycle.js';
|
|
20
|
+
import { createEmailCaptureSingleFlight, submitEmailCapture as submitEmailCapturePrimitive, } from './submit-email-capture.js';
|
|
21
|
+
const isTerminalCompletion = (stepMeta, outcome) => {
|
|
22
|
+
var _a;
|
|
23
|
+
return (outcome.type === 'complete'
|
|
24
|
+
&& Boolean(stepMeta && ((_a = FUNNEL_STEP_CONTRACTS[stepMeta.type]) === null || _a === void 0 ? void 0 : _a.terminal) === true));
|
|
25
|
+
};
|
|
16
26
|
export function resolveFunnelEventAttribution(document, provisionalAttribution) {
|
|
17
27
|
var _a;
|
|
18
28
|
return (_a = getFunnelUserAttribution(document)) !== null && _a !== void 0 ? _a : provisionalAttribution;
|
|
@@ -31,6 +41,176 @@ export function takeInitialFunnelAttributionMetadata(tracked, attributionReady,
|
|
|
31
41
|
const metadata = buildReadyFunnelAttributionEventMetadata(attributionReady, attribution);
|
|
32
42
|
return { tracked: metadata !== null, metadata };
|
|
33
43
|
}
|
|
44
|
+
const EXIT_REASONS = new Set(['back', 'replace', 'cancel', 'abandon']);
|
|
45
|
+
const MAX_NAVIGATION_SELECTED_KEYS = 128;
|
|
46
|
+
const MAX_NAVIGATION_SELECTED_KEY_LENGTH = 128;
|
|
47
|
+
const MAX_NAVIGATION_SELECTED_STRING_LENGTH = 4096;
|
|
48
|
+
const MAX_NAVIGATION_SELECTED_DEPTH = 16;
|
|
49
|
+
const MAX_NAVIGATION_SELECTED_NODES = 512;
|
|
50
|
+
const INVALID_SELECTED_VALUE = Symbol('invalid-selected-value');
|
|
51
|
+
const copyOwnDataProperties = (input, maxKeys) => {
|
|
52
|
+
try {
|
|
53
|
+
const keys = Reflect.ownKeys(input);
|
|
54
|
+
if (keys.length > maxKeys || keys.some((key) => typeof key !== 'string')) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const copy = Object.create(null);
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
60
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
Object.defineProperty(copy, key, {
|
|
64
|
+
configurable: false,
|
|
65
|
+
enumerable: true,
|
|
66
|
+
value: descriptor.value,
|
|
67
|
+
writable: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze(copy);
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const canonicalizeSelected = (input) => {
|
|
77
|
+
let nodes = 0;
|
|
78
|
+
const ancestors = new WeakSet();
|
|
79
|
+
const visit = (value, depth) => {
|
|
80
|
+
nodes += 1;
|
|
81
|
+
if (nodes > MAX_NAVIGATION_SELECTED_NODES || depth > MAX_NAVIGATION_SELECTED_DEPTH) {
|
|
82
|
+
return INVALID_SELECTED_VALUE;
|
|
83
|
+
}
|
|
84
|
+
if (value === null || typeof value === 'boolean') {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'number') {
|
|
88
|
+
return Number.isFinite(value) ? value : INVALID_SELECTED_VALUE;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
return value.length <= MAX_NAVIGATION_SELECTED_STRING_LENGTH
|
|
92
|
+
? value
|
|
93
|
+
: INVALID_SELECTED_VALUE;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value !== 'object' || ancestors.has(value)) {
|
|
96
|
+
return INVALID_SELECTED_VALUE;
|
|
97
|
+
}
|
|
98
|
+
const isArray = Array.isArray(value);
|
|
99
|
+
const prototype = Object.getPrototypeOf(value);
|
|
100
|
+
if (isArray ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
|
|
101
|
+
return INVALID_SELECTED_VALUE;
|
|
102
|
+
}
|
|
103
|
+
ancestors.add(value);
|
|
104
|
+
try {
|
|
105
|
+
const keys = Reflect.ownKeys(value);
|
|
106
|
+
if (keys.some((key) => typeof key !== 'string')) {
|
|
107
|
+
return INVALID_SELECTED_VALUE;
|
|
108
|
+
}
|
|
109
|
+
if (isArray) {
|
|
110
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
111
|
+
const length = lengthDescriptor && 'value' in lengthDescriptor
|
|
112
|
+
? lengthDescriptor.value
|
|
113
|
+
: null;
|
|
114
|
+
if (!Number.isSafeInteger(length)
|
|
115
|
+
|| length < 0
|
|
116
|
+
|| length > MAX_NAVIGATION_SELECTED_NODES
|
|
117
|
+
|| keys.length !== length + 1) {
|
|
118
|
+
return INVALID_SELECTED_VALUE;
|
|
119
|
+
}
|
|
120
|
+
const copy = [];
|
|
121
|
+
for (let index = 0; index < length; index += 1) {
|
|
122
|
+
const key = String(index);
|
|
123
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
124
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
125
|
+
return INVALID_SELECTED_VALUE;
|
|
126
|
+
}
|
|
127
|
+
const nested = visit(descriptor.value, depth + 1);
|
|
128
|
+
if (nested === INVALID_SELECTED_VALUE) {
|
|
129
|
+
return INVALID_SELECTED_VALUE;
|
|
130
|
+
}
|
|
131
|
+
copy.push(nested);
|
|
132
|
+
}
|
|
133
|
+
return Object.freeze(copy);
|
|
134
|
+
}
|
|
135
|
+
if (keys.length > MAX_NAVIGATION_SELECTED_KEYS) {
|
|
136
|
+
return INVALID_SELECTED_VALUE;
|
|
137
|
+
}
|
|
138
|
+
const copy = Object.create(null);
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
if (key.length > MAX_NAVIGATION_SELECTED_KEY_LENGTH) {
|
|
141
|
+
return INVALID_SELECTED_VALUE;
|
|
142
|
+
}
|
|
143
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
144
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
145
|
+
return INVALID_SELECTED_VALUE;
|
|
146
|
+
}
|
|
147
|
+
const nested = visit(descriptor.value, depth + 1);
|
|
148
|
+
if (nested === INVALID_SELECTED_VALUE) {
|
|
149
|
+
return INVALID_SELECTED_VALUE;
|
|
150
|
+
}
|
|
151
|
+
Object.defineProperty(copy, key, {
|
|
152
|
+
configurable: false,
|
|
153
|
+
enumerable: true,
|
|
154
|
+
value: nested,
|
|
155
|
+
writable: false,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return Object.freeze(copy);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
ancestors.delete(value);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
const selected = visit(input, 0);
|
|
166
|
+
return selected === INVALID_SELECTED_VALUE || selected === null || Array.isArray(selected)
|
|
167
|
+
? null
|
|
168
|
+
: selected;
|
|
169
|
+
}
|
|
170
|
+
catch (_a) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
function resolveNavigationOutcome(input, stepContractVersion) {
|
|
175
|
+
if (input === undefined) {
|
|
176
|
+
return stepContractVersion === LEGACY_UNVERSIONED_STEP_CONTRACT_VERSION
|
|
177
|
+
? { type: 'complete' }
|
|
178
|
+
: null;
|
|
179
|
+
}
|
|
180
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const record = copyOwnDataProperties(input, 2);
|
|
184
|
+
if (!record) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const keys = Object.keys(record);
|
|
189
|
+
if (record.type === 'complete') {
|
|
190
|
+
if (keys.some((key) => key !== 'type' && key !== 'selected')) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (record.selected === undefined) {
|
|
194
|
+
return { type: 'complete' };
|
|
195
|
+
}
|
|
196
|
+
if (record.selected === null || typeof record.selected !== 'object' || Array.isArray(record.selected)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const selected = canonicalizeSelected(record.selected);
|
|
200
|
+
return selected ? { type: 'complete', selected } : null;
|
|
201
|
+
}
|
|
202
|
+
if (record.type === 'exit'
|
|
203
|
+
&& keys.every((key) => key === 'type' || key === 'reason')
|
|
204
|
+
&& typeof record.reason === 'string'
|
|
205
|
+
&& EXIT_REASONS.has(record.reason)) {
|
|
206
|
+
return { type: 'exit', reason: record.reason };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (_a) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
34
214
|
export function computeRenderSuspended(input) {
|
|
35
215
|
var _a;
|
|
36
216
|
if (input.isPreviewRuntime) {
|
|
@@ -89,15 +269,16 @@ export function resolveRenderedExperimentStepId(input) {
|
|
|
89
269
|
const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
|
|
90
270
|
return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
|
|
91
271
|
}
|
|
92
|
-
export function useFunnelFlowController({ analytics, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
|
|
272
|
+
export function useFunnelFlowController({ api = apiService, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
|
|
93
273
|
var _a, _b;
|
|
94
274
|
const [runtimeMode, setRuntimeMode] = useRuntimeMode();
|
|
95
|
-
const [editorModeEnabled, setEditorModeEnabled] = useState(
|
|
275
|
+
const [editorModeEnabled, setEditorModeEnabled] = useState(() => isEditorEnabled());
|
|
96
276
|
const [editorPanelExpanded, setEditorPanelExpanded] = useState(false);
|
|
97
277
|
const [postHogReady, setPostHogReady] = useState(() => isPostHogReady());
|
|
98
278
|
const [editorVariantOverrides, setEditorVariantOverrides] = useState({});
|
|
99
279
|
const shouldLockToInitialStep = lockToInitialStep || isPreviewStepLockRequested();
|
|
100
280
|
const isPreviewRuntime = useMemo(() => isPreviewFrameRuntime(), []);
|
|
281
|
+
const lifecycleEventsSuppressed = isPreviewRuntime || editorModeEnabled;
|
|
101
282
|
const resolveRenderableStepId = useCallback((stepId) => {
|
|
102
283
|
if (!stepId || !isFunnelStepId(stepId)) {
|
|
103
284
|
return null;
|
|
@@ -130,7 +311,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
130
311
|
const [activeStepId, setActiveStepId] = useState(safeInitialStepId);
|
|
131
312
|
const [attributes, setAttributes] = useState(() => (Object.assign({}, (initialAttributes || {}))));
|
|
132
313
|
const [user, setUser] = useState(() => ({
|
|
133
|
-
id:
|
|
314
|
+
id: api.getOrCreateClientUserId(),
|
|
134
315
|
name: '',
|
|
135
316
|
email: '',
|
|
136
317
|
attributes: {},
|
|
@@ -143,10 +324,21 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
143
324
|
const attributesRef = useRef(attributes);
|
|
144
325
|
const postHogFeatureFlagsRef = useRef({});
|
|
145
326
|
const prevStepIdRef = useRef(null);
|
|
146
|
-
const
|
|
147
|
-
const
|
|
327
|
+
const funnelStartedTrackedRef = useRef(false);
|
|
328
|
+
const firstStepViewedVisitRef = useRef(null);
|
|
329
|
+
const firstStepClickedVisitRef = useRef(null);
|
|
330
|
+
// One controller mount is one funnel run. A full reload intentionally begins a new run.
|
|
331
|
+
const funnelCompletionClaimedRef = useRef(false);
|
|
148
332
|
const currentUserIdRef = useRef(user.id);
|
|
149
333
|
const initialAttributionRef = useRef(null);
|
|
334
|
+
const choiceCompletionMarkerRef = useRef(null);
|
|
335
|
+
const choiceCommitInFlightRef = useRef(null);
|
|
336
|
+
const choiceNavigationRetryRef = useRef(null);
|
|
337
|
+
const stepLifecycleStateRef = useRef(createFunnelStepLifecycleState());
|
|
338
|
+
const activeVisitTokenRef = useRef(null);
|
|
339
|
+
const emailCaptureSingleFlightRef = useRef(createEmailCaptureSingleFlight());
|
|
340
|
+
const pendingEmailCaptureNavigationRef = useRef(null);
|
|
341
|
+
const appliedInitialStepIdRef = useRef(safeInitialStepId);
|
|
150
342
|
const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
|
|
151
343
|
const activeStepMeta = stepById[safeActiveStepId];
|
|
152
344
|
const renderSuspended = useMemo(() => computeRenderSuspended({
|
|
@@ -184,6 +376,16 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
184
376
|
}), [activeExperimentForStep, editorVariantOverrides, postHogReady, resolveRenderableStepId, safeActiveStepId]);
|
|
185
377
|
const renderedStepMeta = stepById[renderedStepId];
|
|
186
378
|
const activeStepComponent = stepComponentById[renderedStepId];
|
|
379
|
+
const reportChoiceDiagnostic = useCallback((diagnostic) => {
|
|
380
|
+
logger.error(`[FunnelFlow] ${diagnostic.code}: ${diagnostic.reason}`);
|
|
381
|
+
dispatchWindowCustomEvent('funnel:contract-diagnostic', {
|
|
382
|
+
code: diagnostic.code,
|
|
383
|
+
stepId: diagnostic.stepId,
|
|
384
|
+
reason: diagnostic.reason,
|
|
385
|
+
repair: diagnostic.repair,
|
|
386
|
+
});
|
|
387
|
+
}, []);
|
|
388
|
+
const choiceGuardEnvironment = isDevelopmentRuntime() ? 'development' : 'production';
|
|
187
389
|
const postHogFeatureFlags = useMemo(() => {
|
|
188
390
|
if (!postHogReady || isPreviewRuntime) {
|
|
189
391
|
return {};
|
|
@@ -193,12 +395,20 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
193
395
|
return variantKey ? [[experiment.experimentId, variantKey]] : [];
|
|
194
396
|
}));
|
|
195
397
|
}, [funnelExperiments, isPreviewRuntime, postHogReady]);
|
|
196
|
-
const
|
|
197
|
-
var _a;
|
|
398
|
+
const resolveNavigationTarget = useCallback((stepId) => {
|
|
399
|
+
var _a, _b;
|
|
198
400
|
if (shouldLockToInitialStep) {
|
|
199
|
-
return;
|
|
401
|
+
return null;
|
|
200
402
|
}
|
|
201
403
|
const safeStepId = (_a = resolveRenderableStepId(stepId)) !== null && _a !== void 0 ? _a : safeInitialStepId;
|
|
404
|
+
const activeVisitStepId = (_b = activeVisitTokenRef.current) === null || _b === void 0 ? void 0 : _b.stepId;
|
|
405
|
+
return safeStepId === safeActiveStepId
|
|
406
|
+
|| safeStepId === renderedStepId
|
|
407
|
+
|| safeStepId === activeVisitStepId
|
|
408
|
+
? null
|
|
409
|
+
: safeStepId;
|
|
410
|
+
}, [renderedStepId, resolveRenderableStepId, safeActiveStepId, safeInitialStepId, shouldLockToInitialStep]);
|
|
411
|
+
const navigateToStep = useCallback((safeStepId) => {
|
|
202
412
|
const stepPath = getPathForStep(safeStepId);
|
|
203
413
|
const nextLocation = buildHostedStepLocation({
|
|
204
414
|
currentHref: window.location.href,
|
|
@@ -215,16 +425,38 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
215
425
|
}
|
|
216
426
|
}
|
|
217
427
|
setActiveStepId(safeStepId);
|
|
218
|
-
}, [getPathForStep
|
|
428
|
+
}, [getPathForStep]);
|
|
219
429
|
const setAnswer = useCallback((key, value) => {
|
|
430
|
+
if (!enforceChoiceBypassGuard({
|
|
431
|
+
operation: 'setAnswer',
|
|
432
|
+
activeStepId: renderedStepId,
|
|
433
|
+
key: String(key),
|
|
434
|
+
stepById,
|
|
435
|
+
stepContractVersion,
|
|
436
|
+
environment: choiceGuardEnvironment,
|
|
437
|
+
reportDiagnostic: reportChoiceDiagnostic,
|
|
438
|
+
})) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
220
441
|
setAttributes((prev) => (Object.assign(Object.assign({}, prev), { [key]: value })));
|
|
221
|
-
}, []);
|
|
442
|
+
}, [choiceGuardEnvironment, renderedStepId, reportChoiceDiagnostic, stepById, stepContractVersion]);
|
|
222
443
|
const getAnswer = useCallback((key) => {
|
|
223
444
|
return attributesRef.current[key];
|
|
224
445
|
}, []);
|
|
225
446
|
const setAttribute = useCallback((key, value) => {
|
|
447
|
+
if (!enforceChoiceBypassGuard({
|
|
448
|
+
operation: 'setAttribute',
|
|
449
|
+
activeStepId: renderedStepId,
|
|
450
|
+
key,
|
|
451
|
+
stepById,
|
|
452
|
+
stepContractVersion,
|
|
453
|
+
environment: choiceGuardEnvironment,
|
|
454
|
+
reportDiagnostic: reportChoiceDiagnostic,
|
|
455
|
+
})) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
226
458
|
setAttributes((prev) => (Object.assign(Object.assign({}, prev), { [key]: value })));
|
|
227
|
-
}, []);
|
|
459
|
+
}, [choiceGuardEnvironment, renderedStepId, reportChoiceDiagnostic, stepById, stepContractVersion]);
|
|
228
460
|
useEffect(() => {
|
|
229
461
|
currentUserIdRef.current = user.id;
|
|
230
462
|
}, [user.id]);
|
|
@@ -251,9 +483,10 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
251
483
|
return () => window.clearInterval(intervalId);
|
|
252
484
|
}, [postHogReady]);
|
|
253
485
|
const resolveNextStepId = useCallback((stepId, options) => {
|
|
486
|
+
var _a;
|
|
254
487
|
return resolveNextStepFromContext({
|
|
255
488
|
stepId,
|
|
256
|
-
attributes: attributesRef.current,
|
|
489
|
+
attributes: (_a = options === null || options === void 0 ? void 0 : options.attributes) !== null && _a !== void 0 ? _a : attributesRef.current,
|
|
257
490
|
safeInitialStepId,
|
|
258
491
|
resolveRenderableStepId,
|
|
259
492
|
getConfiguredNextStepId: (currentStepId, context) => resolveConfiguredNextStep(currentStepId, context),
|
|
@@ -268,15 +501,10 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
268
501
|
resolveRenderableStepId,
|
|
269
502
|
safeInitialStepId,
|
|
270
503
|
]);
|
|
271
|
-
const goNext = useCallback(() => {
|
|
272
|
-
goToStep(resolveNextStepId(renderedStepId, {
|
|
273
|
-
ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
|
|
274
|
-
}));
|
|
275
|
-
}, [activeExperimentForStep, goToStep, renderedStepId, resolveNextStepId, safeActiveStepId]);
|
|
276
504
|
useEffect(() => {
|
|
277
|
-
const localUserId =
|
|
505
|
+
const localUserId = api.getOrCreateClientUserId();
|
|
278
506
|
const urlUserAttributes = resolveUrlUserAttributes();
|
|
279
|
-
const bootstrapCandidateUserId =
|
|
507
|
+
const bootstrapCandidateUserId = api.getBootstrapCandidateUserId(localUserId);
|
|
280
508
|
const initialAttribution = collectCurrentFunnelAttribution();
|
|
281
509
|
initialAttributionRef.current = initialAttribution;
|
|
282
510
|
setUserBootstrapped(isPreviewRuntime);
|
|
@@ -297,7 +525,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
297
525
|
logger.error('Failed to bootstrap PostHog feature flags:', error);
|
|
298
526
|
setPostHogReady(true);
|
|
299
527
|
});
|
|
300
|
-
const sessionBootstrap =
|
|
528
|
+
const sessionBootstrap = api
|
|
301
529
|
.bootstrapSession({
|
|
302
530
|
userId: bootstrapCandidateUserId || localUserId,
|
|
303
531
|
email: urlUserAttributes.email || undefined,
|
|
@@ -316,7 +544,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
316
544
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: sessionUser.id || prev.id, name: sessionUser.name || prev.name, email: sessionUser.email || prev.email, document: sessionUser.document, attributes: sessionAttributes ? Object.assign(Object.assign({}, sessionAttributes), prev.attributes) : prev.attributes })));
|
|
317
545
|
setUserBootstrapped(true);
|
|
318
546
|
setAttributionReady(true);
|
|
319
|
-
void
|
|
547
|
+
void api.pingUserContext(sessionUser.id)
|
|
320
548
|
.then((pingedUser) => {
|
|
321
549
|
if (!pingedUser) {
|
|
322
550
|
return;
|
|
@@ -349,11 +577,11 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
349
577
|
logger.error('Failed to identify PostHog user:', error);
|
|
350
578
|
});
|
|
351
579
|
return cancelAttributionFailOpen;
|
|
352
|
-
}, [isPreviewRuntime]);
|
|
353
|
-
const recordStepCompletion = useCallback((
|
|
580
|
+
}, [api, isPreviewRuntime]);
|
|
581
|
+
const recordStepCompletion = useCallback((intent, options = {}) => {
|
|
354
582
|
var _a;
|
|
355
|
-
const meta = stepById[stepId];
|
|
356
|
-
if (!meta) {
|
|
583
|
+
const meta = stepById[intent.visit.stepId];
|
|
584
|
+
if (!meta || lifecycleEventsSuppressed) {
|
|
357
585
|
return;
|
|
358
586
|
}
|
|
359
587
|
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
@@ -361,8 +589,8 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
361
589
|
return;
|
|
362
590
|
}
|
|
363
591
|
let choices;
|
|
364
|
-
if (explicitChoices) {
|
|
365
|
-
choices = explicitChoices;
|
|
592
|
+
if (options.explicitChoices !== undefined) {
|
|
593
|
+
choices = options.explicitChoices;
|
|
366
594
|
}
|
|
367
595
|
else {
|
|
368
596
|
const snapshot = attributesAtStepStart.current;
|
|
@@ -380,29 +608,14 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
380
608
|
choices,
|
|
381
609
|
};
|
|
382
610
|
const stepType = meta.type;
|
|
383
|
-
const
|
|
611
|
+
const completionAttributes = Object.assign({}, attributesRef.current);
|
|
384
612
|
dispatchWindowCustomEvent('funnel:step-completed', record);
|
|
385
|
-
if (!isPreviewRuntime) {
|
|
386
|
-
const analyticsRuntimeMode = getRuntimeMode();
|
|
387
|
-
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepCompleted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
388
|
-
userId: currentUserIdRef.current,
|
|
389
|
-
environment: analyticsRuntimeMode,
|
|
390
|
-
stepId: record.stepId,
|
|
391
|
-
stepName: record.stepName,
|
|
392
|
-
stepType,
|
|
393
|
-
startedAt,
|
|
394
|
-
endedAt: record.completedAt,
|
|
395
|
-
selected: record.choices,
|
|
396
|
-
metadata: attributionMetadata,
|
|
397
|
-
featureFlags: postHogFeatureFlagsRef.current,
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
613
|
setUser((prev) => {
|
|
401
614
|
var _a, _b;
|
|
402
615
|
const updatedSteps = [...((_a = prev.completedSteps) !== null && _a !== void 0 ? _a : []), record];
|
|
403
|
-
const updatedUser = Object.assign(Object.assign({}, prev), { attributes: Object.assign(Object.assign({}, prev.attributes),
|
|
616
|
+
const updatedUser = Object.assign(Object.assign({}, prev), { attributes: Object.assign(Object.assign({}, prev.attributes), completionAttributes), document: prev.document, completedSteps: updatedSteps });
|
|
404
617
|
if (!isPreviewRuntime) {
|
|
405
|
-
|
|
618
|
+
api
|
|
406
619
|
.updateUser({
|
|
407
620
|
id: updatedUser.id,
|
|
408
621
|
name: updatedUser.name,
|
|
@@ -418,14 +631,347 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
418
631
|
}
|
|
419
632
|
return updatedUser;
|
|
420
633
|
});
|
|
421
|
-
|
|
634
|
+
if (!isPreviewRuntime) {
|
|
635
|
+
const analyticsRuntimeMode = getRuntimeMode();
|
|
636
|
+
try {
|
|
637
|
+
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepCompleted) === null || _a === void 0 ? void 0 : _a.call(analytics, Object.assign(Object.assign({ userId: currentUserIdRef.current, environment: analyticsRuntimeMode, stepId: record.stepId, stepName: record.stepName, stepType, startedAt: intent.visit.startedAt, endedAt: record.completedAt, stepContractVersion }, (options.omitSelected ? {} : { selected: record.choices })), { metadata: Object.assign(Object.assign(Object.assign({}, attributionMetadata), options.metadata), { step_contract_version: stepContractVersion }), featureFlags: postHogFeatureFlagsRef.current }));
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
logger.error('Failed to track step completion:', error);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}, [analytics, api, attributionReady, isPreviewRuntime, lifecycleEventsSuppressed, stepById, stepContractVersion]);
|
|
644
|
+
const emitStepExit = useCallback((intent) => {
|
|
645
|
+
var _a;
|
|
646
|
+
const meta = stepById[intent.visit.stepId];
|
|
647
|
+
if (!meta || lifecycleEventsSuppressed) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
651
|
+
if (attributionMetadata === null) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepExited) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
655
|
+
userId: currentUserIdRef.current,
|
|
656
|
+
environment: getRuntimeMode(),
|
|
657
|
+
stepId: meta.id,
|
|
658
|
+
stepName: meta.name || meta.title,
|
|
659
|
+
stepType: meta.type,
|
|
660
|
+
startedAt: intent.visit.startedAt,
|
|
661
|
+
endedAt: new Date().toISOString(),
|
|
662
|
+
exitReason: intent.reason,
|
|
663
|
+
stepContractVersion,
|
|
664
|
+
metadata: Object.assign(Object.assign({}, attributionMetadata), { step_contract_version: stepContractVersion }),
|
|
665
|
+
featureFlags: postHogFeatureFlagsRef.current,
|
|
666
|
+
});
|
|
667
|
+
}, [analytics, attributionReady, lifecycleEventsSuppressed, stepById, stepContractVersion]);
|
|
668
|
+
const claimActiveVisit = useCallback((stepId, outcome, options = {}) => {
|
|
669
|
+
const token = activeVisitTokenRef.current;
|
|
670
|
+
if (!token || token.stepId !== stepId) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
const safeOutcome = resolveNavigationOutcome(outcome, stepContractVersion);
|
|
674
|
+
if (!safeOutcome) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
if (isTerminalCompletion(stepById[stepId], safeOutcome)
|
|
678
|
+
&& options.allowTerminalCompletion !== true) {
|
|
679
|
+
logger.warn('[FunnelFlow] Terminal completion must use completeFunnel().');
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
const intent = finishVisit(stepLifecycleStateRef.current, token.visitId, safeOutcome);
|
|
683
|
+
if (!intent) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
if (intent.type === 'exit') {
|
|
687
|
+
emitStepExit(intent);
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
recordStepCompletion(intent, Object.assign(Object.assign({}, (safeOutcome.type === 'complete' && safeOutcome.selected !== undefined
|
|
691
|
+
? { explicitChoices: Object.assign({}, safeOutcome.selected) }
|
|
692
|
+
: {})), { metadata: options.metadata, omitSelected: options.omitSelected }));
|
|
693
|
+
return true;
|
|
694
|
+
}, [emitStepExit, recordStepCompletion, stepById, stepContractVersion]);
|
|
695
|
+
const claimCurrentVisit = useCallback((outcome) => {
|
|
696
|
+
const token = activeVisitTokenRef.current;
|
|
697
|
+
return token ? claimActiveVisit(token.stepId, outcome) : false;
|
|
698
|
+
}, [claimActiveVisit]);
|
|
699
|
+
const goToStep = useCallback((stepId, outcome) => {
|
|
700
|
+
const resolvedOutcome = resolveNavigationOutcome(outcome, stepContractVersion);
|
|
701
|
+
const targetStepId = resolveNavigationTarget(stepId);
|
|
702
|
+
if (!resolvedOutcome || !targetStepId) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (isTerminalCompletion(stepById[renderedStepId], resolvedOutcome)) {
|
|
706
|
+
logger.warn('[FunnelFlow] Terminal completion navigation must use completeFunnel().');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
claimCurrentVisit(resolvedOutcome);
|
|
710
|
+
navigateToStep(targetStepId);
|
|
711
|
+
}, [
|
|
712
|
+
claimCurrentVisit,
|
|
713
|
+
navigateToStep,
|
|
714
|
+
renderedStepId,
|
|
715
|
+
resolveNavigationTarget,
|
|
716
|
+
stepById,
|
|
717
|
+
stepContractVersion,
|
|
718
|
+
]);
|
|
719
|
+
const goNext = useCallback(() => {
|
|
720
|
+
goToStep(resolveNextStepId(renderedStepId, {
|
|
721
|
+
ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
|
|
722
|
+
}), { type: 'complete' });
|
|
723
|
+
}, [activeExperimentForStep, goToStep, renderedStepId, resolveNextStepId, safeActiveStepId]);
|
|
724
|
+
const submitEmailCapture = useCallback((email) => {
|
|
725
|
+
var _a, _b;
|
|
726
|
+
const submittedStepId = renderedStepId;
|
|
727
|
+
const submittedStepMeta = stepById[submittedStepId];
|
|
728
|
+
const submittedVisitId = (_b = (_a = activeVisitTokenRef.current) === null || _a === void 0 ? void 0 : _a.visitId) !== null && _b !== void 0 ? _b : null;
|
|
729
|
+
const submittedUserId = currentUserIdRef.current;
|
|
730
|
+
const navigateManifestTarget = (stepId) => {
|
|
731
|
+
if (!isFunnelStepId(stepId)) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const targetStepId = resolveNavigationTarget(stepId);
|
|
735
|
+
if (targetStepId) {
|
|
736
|
+
navigateToStep(targetStepId);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
return submitEmailCapturePrimitive({
|
|
740
|
+
email,
|
|
741
|
+
isPreview: isPreviewRuntime,
|
|
742
|
+
singleFlight: emailCaptureSingleFlightRef.current,
|
|
743
|
+
retryPendingNavigation: () => {
|
|
744
|
+
var _a;
|
|
745
|
+
const pending = pendingEmailCaptureNavigationRef.current;
|
|
746
|
+
if (!pending) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
if (pending.visitId !== ((_a = activeVisitTokenRef.current) === null || _a === void 0 ? void 0 : _a.visitId)
|
|
750
|
+
|| pending.stepId !== submittedStepId) {
|
|
751
|
+
pendingEmailCaptureNavigationRef.current = null;
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
navigateManifestTarget(pending.targetStepId);
|
|
755
|
+
pendingEmailCaptureNavigationRef.current = null;
|
|
756
|
+
return true;
|
|
757
|
+
},
|
|
758
|
+
persist: (normalizedEmail) => api.captureEmail({
|
|
759
|
+
userId: submittedUserId,
|
|
760
|
+
email: normalizedEmail,
|
|
761
|
+
environment: getRuntimeMode(),
|
|
762
|
+
}),
|
|
763
|
+
applyUser: (capturedUser) => {
|
|
764
|
+
currentUserIdRef.current = capturedUser.id;
|
|
765
|
+
setUser((prev) => (Object.assign(Object.assign(Object.assign({}, prev), capturedUser), { completedSteps: prev.completedSteps })));
|
|
766
|
+
},
|
|
767
|
+
applyPreviewEmail: (normalizedEmail) => {
|
|
768
|
+
setUser((prev) => (Object.assign(Object.assign({}, prev), { email: normalizedEmail })));
|
|
769
|
+
},
|
|
770
|
+
fanOutBrowserDestinations: (eventId) => {
|
|
771
|
+
var _a;
|
|
772
|
+
if (!submittedStepMeta) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackEmailCapturedBrowserDestinations) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
776
|
+
eventId,
|
|
777
|
+
stepId: submittedStepMeta.id,
|
|
778
|
+
stepName: submittedStepMeta.name || submittedStepMeta.title,
|
|
779
|
+
stepType: submittedStepMeta.type,
|
|
780
|
+
});
|
|
781
|
+
},
|
|
782
|
+
completeVisit: () => {
|
|
783
|
+
var _a;
|
|
784
|
+
return (submittedVisitId !== null
|
|
785
|
+
&& ((_a = activeVisitTokenRef.current) === null || _a === void 0 ? void 0 : _a.visitId) === submittedVisitId
|
|
786
|
+
&& claimActiveVisit(submittedStepId, { type: 'complete' }));
|
|
787
|
+
},
|
|
788
|
+
resolveNextStep: () => {
|
|
789
|
+
const targetStepId = resolveNextStepId(submittedStepId, {
|
|
790
|
+
ignoreExperiment: Boolean(activeExperimentForStep && submittedStepId === safeActiveStepId),
|
|
791
|
+
});
|
|
792
|
+
if (submittedVisitId !== null) {
|
|
793
|
+
pendingEmailCaptureNavigationRef.current = {
|
|
794
|
+
visitId: submittedVisitId,
|
|
795
|
+
stepId: submittedStepId,
|
|
796
|
+
targetStepId,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return targetStepId;
|
|
800
|
+
},
|
|
801
|
+
navigate: (stepId) => {
|
|
802
|
+
navigateManifestTarget(stepId);
|
|
803
|
+
pendingEmailCaptureNavigationRef.current = null;
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
}, [
|
|
807
|
+
activeExperimentForStep,
|
|
808
|
+
api,
|
|
809
|
+
analytics,
|
|
810
|
+
claimActiveVisit,
|
|
811
|
+
isFunnelStepId,
|
|
812
|
+
isPreviewRuntime,
|
|
813
|
+
navigateToStep,
|
|
814
|
+
renderedStepId,
|
|
815
|
+
resolveNavigationTarget,
|
|
816
|
+
resolveNextStepId,
|
|
817
|
+
safeActiveStepId,
|
|
818
|
+
stepById,
|
|
819
|
+
]);
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
const abandonCurrentVisit = (event) => {
|
|
822
|
+
if (event.persisted) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
claimCurrentVisit({ type: 'exit', reason: 'abandon' });
|
|
826
|
+
};
|
|
827
|
+
window.addEventListener('pagehide', abandonCurrentVisit);
|
|
828
|
+
return () => window.removeEventListener('pagehide', abandonCurrentVisit);
|
|
829
|
+
}, [claimCurrentVisit]);
|
|
422
830
|
const completeStep = useCallback((stepId, choices) => {
|
|
423
|
-
|
|
424
|
-
|
|
831
|
+
if (!enforceChoiceBypassGuard({
|
|
832
|
+
operation: 'completeStep',
|
|
833
|
+
activeStepId: renderedStepId,
|
|
834
|
+
sourceStepId: stepId,
|
|
835
|
+
stepById,
|
|
836
|
+
stepContractVersion,
|
|
837
|
+
environment: choiceGuardEnvironment,
|
|
838
|
+
reportDiagnostic: reportChoiceDiagnostic,
|
|
839
|
+
})) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
claimActiveVisit(stepId, Object.assign({ type: 'complete' }, (choices === undefined ? {} : { selected: choices })));
|
|
843
|
+
}, [
|
|
844
|
+
choiceGuardEnvironment,
|
|
845
|
+
claimActiveVisit,
|
|
846
|
+
renderedStepId,
|
|
847
|
+
reportChoiceDiagnostic,
|
|
848
|
+
stepById,
|
|
849
|
+
stepContractVersion,
|
|
850
|
+
]);
|
|
851
|
+
const completeFunnel = useCallback((stepId) => {
|
|
852
|
+
var _a, _b;
|
|
853
|
+
const stepMeta = stepById[stepId];
|
|
854
|
+
if (!stepMeta
|
|
855
|
+
|| ((_a = FUNNEL_STEP_CONTRACTS[stepMeta.type]) === null || _a === void 0 ? void 0 : _a.terminal) !== true) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
859
|
+
if (attributionMetadata === null
|
|
860
|
+
|| !claimActiveVisit(stepId, { type: 'complete' }, { allowTerminalCompletion: true })) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const alreadyCompletedThisRun = funnelCompletionClaimedRef.current;
|
|
864
|
+
funnelCompletionClaimedRef.current = true;
|
|
865
|
+
if (lifecycleEventsSuppressed || alreadyCompletedThisRun) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const occurredAt = new Date().toISOString();
|
|
869
|
+
try {
|
|
870
|
+
(_b = analytics === null || analytics === void 0 ? void 0 : analytics.trackFunnelCompleted) === null || _b === void 0 ? void 0 : _b.call(analytics, {
|
|
871
|
+
userId: currentUserIdRef.current,
|
|
872
|
+
environment: getRuntimeMode(),
|
|
873
|
+
stepId: stepMeta.id,
|
|
874
|
+
stepName: stepMeta.name || stepMeta.title,
|
|
875
|
+
stepType: stepMeta.type,
|
|
876
|
+
stepContractVersion,
|
|
877
|
+
occurredAt,
|
|
878
|
+
metadata: Object.assign(Object.assign({}, attributionMetadata), { step_contract_version: stepContractVersion }),
|
|
879
|
+
featureFlags: postHogFeatureFlagsRef.current,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
logger.error('Failed to track funnel completion:', error);
|
|
884
|
+
}
|
|
885
|
+
if (!(analytics === null || analytics === void 0 ? void 0 : analytics.flush)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const flushResult = analytics.flush();
|
|
890
|
+
void Promise.resolve(flushResult).catch((error) => {
|
|
891
|
+
logger.error('Failed to flush funnel completion:', error);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
logger.error('Failed to flush funnel completion:', error);
|
|
896
|
+
}
|
|
897
|
+
}, [
|
|
898
|
+
analytics,
|
|
899
|
+
attributionReady,
|
|
900
|
+
claimActiveVisit,
|
|
901
|
+
lifecycleEventsSuppressed,
|
|
902
|
+
stepById,
|
|
903
|
+
stepContractVersion,
|
|
904
|
+
]);
|
|
905
|
+
const commitChoiceCompletion = useCallback((stepMeta, result) => {
|
|
906
|
+
runAtomicChoiceCommit({
|
|
907
|
+
activeStepId: renderedStepId,
|
|
908
|
+
activeStepMeta: renderedStepMeta,
|
|
909
|
+
stepMeta,
|
|
910
|
+
result,
|
|
911
|
+
environment: choiceGuardEnvironment,
|
|
912
|
+
getAttributes: () => attributesRef.current,
|
|
913
|
+
writeAttributes: (nextAttributes) => {
|
|
914
|
+
attributesRef.current = nextAttributes;
|
|
915
|
+
setAttributes(nextAttributes);
|
|
916
|
+
},
|
|
917
|
+
isInFlight: () => choiceCommitInFlightRef.current === renderedStepId,
|
|
918
|
+
beginCommit: () => {
|
|
919
|
+
choiceCommitInFlightRef.current = renderedStepId;
|
|
920
|
+
},
|
|
921
|
+
cancelCommit: () => {
|
|
922
|
+
choiceCommitInFlightRef.current = null;
|
|
923
|
+
},
|
|
924
|
+
recordCompletion: (completion) => {
|
|
925
|
+
let claimed;
|
|
926
|
+
try {
|
|
927
|
+
claimed = claimActiveVisit(renderedStepId, Object.assign({ type: 'complete' }, (completion.selected === undefined ? {} : { selected: completion.selected })), {
|
|
928
|
+
metadata: completion.metadata,
|
|
929
|
+
omitSelected: completion.selected === undefined,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
choiceNavigationRetryRef.current = renderedStepId;
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
if (claimed) {
|
|
937
|
+
choiceNavigationRetryRef.current = null;
|
|
938
|
+
}
|
|
939
|
+
return claimed || choiceNavigationRetryRef.current === renderedStepId;
|
|
940
|
+
},
|
|
941
|
+
markCompletedBeforeNavigation: () => {
|
|
942
|
+
choiceCompletionMarkerRef.current = renderedStepId;
|
|
943
|
+
},
|
|
944
|
+
clearCompletedBeforeNavigation: (markerWasInstalled) => {
|
|
945
|
+
if (markerWasInstalled) {
|
|
946
|
+
choiceNavigationRetryRef.current = renderedStepId;
|
|
947
|
+
}
|
|
948
|
+
if (choiceCompletionMarkerRef.current === renderedStepId) {
|
|
949
|
+
choiceCompletionMarkerRef.current = null;
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
resolveNext: (proposedAttributes) => resolveNextStepId(renderedStepId, {
|
|
953
|
+
attributes: proposedAttributes,
|
|
954
|
+
ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
|
|
955
|
+
}),
|
|
956
|
+
navigate: (stepId) => goToStep(stepId, { type: 'complete' }),
|
|
957
|
+
reportDiagnostic: reportChoiceDiagnostic,
|
|
958
|
+
});
|
|
959
|
+
}, [
|
|
960
|
+
activeExperimentForStep,
|
|
961
|
+
choiceGuardEnvironment,
|
|
962
|
+
goToStep,
|
|
963
|
+
claimActiveVisit,
|
|
964
|
+
renderedStepId,
|
|
965
|
+
renderedStepMeta,
|
|
966
|
+
reportChoiceDiagnostic,
|
|
967
|
+
resolveNextStepId,
|
|
968
|
+
safeActiveStepId,
|
|
969
|
+
]);
|
|
425
970
|
useEffect(() => {
|
|
426
971
|
var _a, _b, _c;
|
|
427
972
|
if (safeActiveStepId !== activeStepId) {
|
|
428
973
|
logger.warn(`[FunnelFlow] Unknown or non-renderable step "${activeStepId}", falling back to "${safeActiveStepId}".`);
|
|
974
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
429
975
|
setActiveStepId(safeActiveStepId);
|
|
430
976
|
return;
|
|
431
977
|
}
|
|
@@ -437,20 +983,32 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
437
983
|
return;
|
|
438
984
|
}
|
|
439
985
|
if (prevId && prevId !== renderedStepId) {
|
|
440
|
-
|
|
986
|
+
const marker = consumeChoiceCompletionMarker(choiceCompletionMarkerRef.current, prevId);
|
|
987
|
+
choiceCompletionMarkerRef.current = marker.nextMarker;
|
|
988
|
+
if (marker.consumed) {
|
|
989
|
+
choiceCommitInFlightRef.current = null;
|
|
990
|
+
choiceNavigationRetryRef.current = null;
|
|
991
|
+
}
|
|
441
992
|
}
|
|
442
993
|
const startedAt = new Date().toISOString();
|
|
443
|
-
const stepName = (renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.name) || (renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.title) || renderedStepId;
|
|
444
|
-
const stepType = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.type;
|
|
445
|
-
stepStartedAtByIdRef.current[renderedStepId] = startedAt;
|
|
446
|
-
const analyticsRuntimeMode = getRuntimeMode();
|
|
447
|
-
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
448
|
-
prevStepIdRef.current = renderedStepId;
|
|
449
994
|
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
450
|
-
if (attributionMetadata === null) {
|
|
995
|
+
if (attributionMetadata === null || !renderedStepMeta) {
|
|
451
996
|
return;
|
|
452
997
|
}
|
|
453
|
-
|
|
998
|
+
const startIntent = startVisit(stepLifecycleStateRef.current, { stepId: renderedStepId, startedAt });
|
|
999
|
+
if (!startIntent) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
activeVisitTokenRef.current = {
|
|
1003
|
+
visitId: startIntent.visit.visitId,
|
|
1004
|
+
stepId: renderedStepId,
|
|
1005
|
+
};
|
|
1006
|
+
const stepName = renderedStepMeta.name || renderedStepMeta.title;
|
|
1007
|
+
const stepType = renderedStepMeta.type;
|
|
1008
|
+
const analyticsRuntimeMode = getRuntimeMode();
|
|
1009
|
+
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
1010
|
+
prevStepIdRef.current = renderedStepId;
|
|
1011
|
+
if (!lifecycleEventsSuppressed) {
|
|
454
1012
|
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepStarted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
455
1013
|
userId: currentUserIdRef.current,
|
|
456
1014
|
environment: analyticsRuntimeMode,
|
|
@@ -458,54 +1016,122 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
458
1016
|
stepName,
|
|
459
1017
|
stepType,
|
|
460
1018
|
startedAt,
|
|
461
|
-
|
|
1019
|
+
stepContractVersion,
|
|
1020
|
+
metadata: Object.assign(Object.assign({}, attributionMetadata), { step_contract_version: stepContractVersion }),
|
|
462
1021
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
463
1022
|
});
|
|
464
|
-
if (
|
|
465
|
-
const initialAttribution = takeInitialFunnelAttributionMetadata(
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
1023
|
+
if (safeActiveStepId === defaultStepId) {
|
|
1024
|
+
const initialAttribution = takeInitialFunnelAttributionMetadata(funnelStartedTrackedRef.current, attributionReady, initialAttributionRef.current);
|
|
1025
|
+
funnelStartedTrackedRef.current = initialAttribution.tracked;
|
|
1026
|
+
if (initialAttribution.metadata) {
|
|
1027
|
+
(_b = analytics === null || analytics === void 0 ? void 0 : analytics.trackFunnelStarted) === null || _b === void 0 ? void 0 : _b.call(analytics, {
|
|
1028
|
+
userId: currentUserIdRef.current,
|
|
1029
|
+
environment: analyticsRuntimeMode,
|
|
1030
|
+
stepId: renderedStepId,
|
|
1031
|
+
stepName,
|
|
1032
|
+
stepType,
|
|
1033
|
+
stepContractVersion,
|
|
1034
|
+
occurredAt: startedAt,
|
|
1035
|
+
metadata: initialAttribution.metadata,
|
|
1036
|
+
featureFlags: postHogFeatureFlagsRef.current,
|
|
1037
|
+
});
|
|
469
1038
|
}
|
|
470
|
-
(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1039
|
+
if (firstStepViewedVisitRef.current !== startIntent.visit.visitId) {
|
|
1040
|
+
firstStepViewedVisitRef.current = startIntent.visit.visitId;
|
|
1041
|
+
(_c = analytics === null || analytics === void 0 ? void 0 : analytics.trackFirstStepViewed) === null || _c === void 0 ? void 0 : _c.call(analytics, {
|
|
1042
|
+
userId: currentUserIdRef.current,
|
|
1043
|
+
environment: analyticsRuntimeMode,
|
|
1044
|
+
stepId: renderedStepId,
|
|
1045
|
+
stepName,
|
|
1046
|
+
stepType,
|
|
1047
|
+
stepContractVersion,
|
|
1048
|
+
occurredAt: startedAt,
|
|
1049
|
+
metadata: attributionMetadata,
|
|
1050
|
+
featureFlags: postHogFeatureFlagsRef.current,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}, [
|
|
1056
|
+
activeStepId,
|
|
1057
|
+
analytics,
|
|
1058
|
+
attributionReady,
|
|
1059
|
+
claimCurrentVisit,
|
|
1060
|
+
defaultStepId,
|
|
1061
|
+
isPreviewRuntime,
|
|
1062
|
+
lifecycleEventsSuppressed,
|
|
1063
|
+
renderSuspended,
|
|
1064
|
+
renderedStepId,
|
|
1065
|
+
renderedStepMeta,
|
|
1066
|
+
safeActiveStepId,
|
|
1067
|
+
stepContractVersion,
|
|
1068
|
+
]);
|
|
1069
|
+
useEffect(() => {
|
|
1070
|
+
if (lifecycleEventsSuppressed
|
|
1071
|
+
|| renderSuspended
|
|
1072
|
+
|| safeActiveStepId !== defaultStepId
|
|
1073
|
+
|| !renderedStepMeta) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const visit = activeVisitTokenRef.current;
|
|
1077
|
+
if (!visit || visit.stepId !== renderedStepId) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const handleFirstStepClick = () => {
|
|
1081
|
+
var _a;
|
|
1082
|
+
const activeVisit = activeVisitTokenRef.current;
|
|
1083
|
+
if (!activeVisit
|
|
1084
|
+
|| activeVisit.visitId !== visit.visitId
|
|
1085
|
+
|| activeVisit.stepId !== renderedStepId
|
|
1086
|
+
|| firstStepClickedVisitRef.current === visit.visitId) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
1090
|
+
if (attributionMetadata === null) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
firstStepClickedVisitRef.current = visit.visitId;
|
|
1094
|
+
try {
|
|
1095
|
+
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackFirstStepClicked) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
481
1096
|
userId: currentUserIdRef.current,
|
|
482
|
-
environment:
|
|
1097
|
+
environment: getRuntimeMode(),
|
|
483
1098
|
stepId: renderedStepId,
|
|
484
|
-
stepName,
|
|
485
|
-
stepType,
|
|
486
|
-
|
|
487
|
-
|
|
1099
|
+
stepName: renderedStepMeta.name || renderedStepMeta.title,
|
|
1100
|
+
stepType: renderedStepMeta.type,
|
|
1101
|
+
stepContractVersion,
|
|
1102
|
+
occurredAt: new Date().toISOString(),
|
|
1103
|
+
metadata: Object.assign(Object.assign({}, attributionMetadata), { step_contract_version: stepContractVersion }),
|
|
488
1104
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
489
1105
|
});
|
|
490
1106
|
}
|
|
491
|
-
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
logger.error('Failed to track first step click:', error);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
window.addEventListener('click', handleFirstStepClick, true);
|
|
1112
|
+
return () => window.removeEventListener('click', handleFirstStepClick, true);
|
|
492
1113
|
}, [
|
|
493
|
-
activeStepId,
|
|
494
1114
|
analytics,
|
|
495
1115
|
attributionReady,
|
|
496
1116
|
defaultStepId,
|
|
497
|
-
|
|
498
|
-
recordStepCompletion,
|
|
1117
|
+
lifecycleEventsSuppressed,
|
|
499
1118
|
renderSuspended,
|
|
500
1119
|
renderedStepId,
|
|
501
|
-
renderedStepMeta
|
|
502
|
-
renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.title,
|
|
503
|
-
renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.type,
|
|
1120
|
+
renderedStepMeta,
|
|
504
1121
|
safeActiveStepId,
|
|
1122
|
+
stepContractVersion,
|
|
505
1123
|
]);
|
|
506
1124
|
useEffect(() => {
|
|
1125
|
+
if (appliedInitialStepIdRef.current === safeInitialStepId) {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
appliedInitialStepIdRef.current = safeInitialStepId;
|
|
1129
|
+
if (safeActiveStepId === safeInitialStepId) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
507
1133
|
setActiveStepId(safeInitialStepId);
|
|
508
|
-
}, [safeInitialStepId]);
|
|
1134
|
+
}, [claimCurrentVisit, safeActiveStepId, safeInitialStepId]);
|
|
509
1135
|
useEffect(() => {
|
|
510
1136
|
setEditorModeEnabled(isEditorEnabled());
|
|
511
1137
|
}, [safeActiveStepId]);
|
|
@@ -516,18 +1142,34 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
516
1142
|
}, [editorModeEnabled]);
|
|
517
1143
|
useEffect(() => {
|
|
518
1144
|
if (shouldLockToInitialStep) {
|
|
519
|
-
|
|
1145
|
+
if (safeActiveStepId !== safeInitialStepId) {
|
|
1146
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
1147
|
+
setActiveStepId(safeInitialStepId);
|
|
1148
|
+
}
|
|
520
1149
|
return;
|
|
521
1150
|
}
|
|
522
1151
|
const syncStepWithPath = () => {
|
|
523
1152
|
var _a;
|
|
524
1153
|
const resolvedPathStep = getStepIdFromPath(window.location.pathname);
|
|
525
1154
|
const resolvedStep = (_a = resolveRenderableStepId(resolvedPathStep)) !== null && _a !== void 0 ? _a : safeInitialStepId;
|
|
526
|
-
|
|
1155
|
+
const targetStepId = resolveNavigationTarget(resolvedStep);
|
|
1156
|
+
if (!targetStepId) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
claimCurrentVisit({ type: 'exit', reason: 'back' });
|
|
1160
|
+
setActiveStepId(targetStepId);
|
|
527
1161
|
};
|
|
528
1162
|
window.addEventListener('popstate', syncStepWithPath);
|
|
529
1163
|
return () => window.removeEventListener('popstate', syncStepWithPath);
|
|
530
|
-
}, [
|
|
1164
|
+
}, [
|
|
1165
|
+
claimCurrentVisit,
|
|
1166
|
+
getStepIdFromPath,
|
|
1167
|
+
resolveNavigationTarget,
|
|
1168
|
+
resolveRenderableStepId,
|
|
1169
|
+
safeActiveStepId,
|
|
1170
|
+
safeInitialStepId,
|
|
1171
|
+
shouldLockToInitialStep,
|
|
1172
|
+
]);
|
|
531
1173
|
const nextStepId = useMemo(() => resolveNextStepFromContext({
|
|
532
1174
|
stepId: renderedStepId,
|
|
533
1175
|
attributes,
|
|
@@ -563,7 +1205,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
563
1205
|
ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
|
|
564
1206
|
});
|
|
565
1207
|
const timer = window.setTimeout(() => {
|
|
566
|
-
goToStep(autoAdvanceTarget);
|
|
1208
|
+
goToStep(autoAdvanceTarget, { type: 'complete' });
|
|
567
1209
|
}, autoAdvanceMs);
|
|
568
1210
|
return () => window.clearTimeout(timer);
|
|
569
1211
|
}, [
|
|
@@ -577,25 +1219,53 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
577
1219
|
shouldLockToInitialStep,
|
|
578
1220
|
]);
|
|
579
1221
|
const handleContinue = useCallback(() => {
|
|
580
|
-
goToStep(nextStepId);
|
|
1222
|
+
goToStep(nextStepId, { type: 'complete' });
|
|
581
1223
|
}, [goToStep, nextStepId]);
|
|
582
|
-
const goToStepFromContext = useCallback((stepId) => {
|
|
1224
|
+
const goToStepFromContext = useCallback((stepId, outcome) => {
|
|
583
1225
|
if (!isFunnelStepId(stepId)) {
|
|
584
1226
|
return;
|
|
585
1227
|
}
|
|
586
|
-
goToStep(stepId);
|
|
1228
|
+
goToStep(stepId, outcome);
|
|
587
1229
|
}, [goToStep, isFunnelStepId]);
|
|
588
1230
|
const goChoice = useCallback((choice, stepId) => {
|
|
589
1231
|
var _a, _b;
|
|
590
1232
|
const sourceStepId = (_a = resolveRenderableStepId(stepId)) !== null && _a !== void 0 ? _a : renderedStepId;
|
|
591
|
-
|
|
1233
|
+
const guardSourceStepId = stepId && stepById[stepId] ? stepId : sourceStepId;
|
|
1234
|
+
if (!enforceChoiceBypassGuard({
|
|
1235
|
+
operation: 'goChoice',
|
|
1236
|
+
activeStepId: renderedStepId,
|
|
1237
|
+
sourceStepId: guardSourceStepId,
|
|
1238
|
+
stepById,
|
|
1239
|
+
stepContractVersion,
|
|
1240
|
+
environment: choiceGuardEnvironment,
|
|
1241
|
+
reportDiagnostic: reportChoiceDiagnostic,
|
|
1242
|
+
})) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const nextAttributes = writeStepChoice(attributesRef.current, sourceStepId, choice);
|
|
1246
|
+
attributesRef.current = nextAttributes;
|
|
1247
|
+
setAttributes(nextAttributes);
|
|
592
1248
|
const sourceChoiceTargets = getChoiceTargetsForStep(sourceStepId);
|
|
593
1249
|
const choiceTarget = (_b = resolveRenderableStepId(sourceChoiceTargets === null || sourceChoiceTargets === void 0 ? void 0 : sourceChoiceTargets[choice])) !== null && _b !== void 0 ? _b : resolveNextStepId(sourceStepId);
|
|
594
|
-
goToStep(choiceTarget);
|
|
595
|
-
}, [
|
|
1250
|
+
goToStep(choiceTarget, { type: 'complete' });
|
|
1251
|
+
}, [
|
|
1252
|
+
choiceGuardEnvironment,
|
|
1253
|
+
getChoiceTargetsForStep,
|
|
1254
|
+
goToStep,
|
|
1255
|
+
renderedStepId,
|
|
1256
|
+
reportChoiceDiagnostic,
|
|
1257
|
+
resolveNextStepId,
|
|
1258
|
+
resolveRenderableStepId,
|
|
1259
|
+
stepById,
|
|
1260
|
+
stepContractVersion,
|
|
1261
|
+
]);
|
|
596
1262
|
usePreviewBridge({
|
|
597
1263
|
activeStepId: safeActiveStepId,
|
|
598
|
-
onGoToStep:
|
|
1264
|
+
onGoToStep: (stepId) => {
|
|
1265
|
+
if (isFunnelStepId(stepId)) {
|
|
1266
|
+
goToStep(stepId, { type: 'exit', reason: 'replace' });
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
599
1269
|
resolveRenderableStepId,
|
|
600
1270
|
setRuntimeMode,
|
|
601
1271
|
shouldLockToInitialStep,
|
|
@@ -606,11 +1276,12 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
606
1276
|
return;
|
|
607
1277
|
}
|
|
608
1278
|
const selectedVariant = experiment.variants.find((variant) => variant.variantKey === nextVariantKey);
|
|
609
|
-
if (!selectedVariant) {
|
|
1279
|
+
if (!selectedVariant || selectedVariant.routeToStepId === renderedStepId) {
|
|
610
1280
|
return;
|
|
611
1281
|
}
|
|
1282
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
612
1283
|
setEditorVariantOverrides((prev) => (Object.assign(Object.assign({}, prev), { [experiment.experimentId]: selectedVariant.variantKey })));
|
|
613
|
-
}, []);
|
|
1284
|
+
}, [claimCurrentVisit, renderedStepId]);
|
|
614
1285
|
const experimentAssignmentForEditor = useCallback((experiment) => {
|
|
615
1286
|
var _a;
|
|
616
1287
|
const override = editorVariantOverrides[experiment.experimentId];
|
|
@@ -635,30 +1306,40 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
635
1306
|
}
|
|
636
1307
|
return ((_a = experiment.variants[0]) === null || _a === void 0 ? void 0 : _a.variantKey) || '';
|
|
637
1308
|
}, [editorVariantOverrides, postHogReady]);
|
|
638
|
-
const contextValue = useMemo(() =>
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1309
|
+
const contextValue = useMemo(() => {
|
|
1310
|
+
const value = {
|
|
1311
|
+
activeStepId: renderedStepId,
|
|
1312
|
+
featureFlags: postHogFeatureFlags,
|
|
1313
|
+
isBuilder: isPreviewRuntime,
|
|
1314
|
+
goToStep: goToStepFromContext,
|
|
1315
|
+
goNext,
|
|
1316
|
+
goChoice,
|
|
1317
|
+
getChoiceTargets: (stepId) => {
|
|
1318
|
+
var _a;
|
|
1319
|
+
const sourceStepId = (_a = resolveRenderableStepId(stepId)) !== null && _a !== void 0 ? _a : renderedStepId;
|
|
1320
|
+
return getChoiceTargetsForStep(sourceStepId);
|
|
1321
|
+
},
|
|
1322
|
+
setAnswer,
|
|
1323
|
+
getAnswer,
|
|
1324
|
+
answers: attributes,
|
|
1325
|
+
setAttribute,
|
|
1326
|
+
attributes,
|
|
1327
|
+
user,
|
|
1328
|
+
userBootstrapped,
|
|
1329
|
+
setUser,
|
|
1330
|
+
submitEmailCapture,
|
|
1331
|
+
completeStep,
|
|
1332
|
+
completeFunnel,
|
|
1333
|
+
};
|
|
1334
|
+
// The WeakMap stores callback identity only; it never reads captured refs during render.
|
|
1335
|
+
// eslint-disable-next-line react-hooks/refs
|
|
1336
|
+
registerInternalChoiceCommit(value, commitChoiceCompletion);
|
|
1337
|
+
return value;
|
|
1338
|
+
}, [
|
|
660
1339
|
attributes,
|
|
661
1340
|
completeStep,
|
|
1341
|
+
completeFunnel,
|
|
1342
|
+
commitChoiceCompletion,
|
|
662
1343
|
getAnswer,
|
|
663
1344
|
getChoiceTargetsForStep,
|
|
664
1345
|
goChoice,
|
|
@@ -670,6 +1351,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
670
1351
|
resolveRenderableStepId,
|
|
671
1352
|
setAnswer,
|
|
672
1353
|
setAttribute,
|
|
1354
|
+
submitEmailCapture,
|
|
673
1355
|
user,
|
|
674
1356
|
userBootstrapped,
|
|
675
1357
|
]);
|