@funnelsgrove/runtime 0.1.55 → 0.1.59
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-attribution.d.ts +1 -0
- package/dist/runtime/funnel-attribution.js +24 -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/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 +71 -5
- package/dist/runtime/use-funnel-flow-controller.js +850 -116
- 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 +17 -0
- package/dist/services/funnel-sdk.service.js +13 -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
|
@@ -8,14 +8,218 @@ import { buildHostedStepLocation, dispatchWindowCustomEvent, } from './browser-h
|
|
|
8
8
|
import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAdvanceTimer, } from './funnel-flow.js';
|
|
9
9
|
import { usePreviewBridge } from './preview-bridge.js';
|
|
10
10
|
import { resolveExperimentAssignment, resolveForcedExperimentAssignment, } from './experiment-assignment.js';
|
|
11
|
-
import { collectCurrentFunnelAttribution } from './funnel-attribution.js';
|
|
11
|
+
import { buildFunnelAttributionEventMetadata, collectCurrentFunnelAttribution, getFunnelUserAttribution, } from './funnel-attribution.js';
|
|
12
12
|
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
|
+
};
|
|
26
|
+
export function resolveFunnelEventAttribution(document, provisionalAttribution) {
|
|
27
|
+
var _a;
|
|
28
|
+
return (_a = getFunnelUserAttribution(document)) !== null && _a !== void 0 ? _a : provisionalAttribution;
|
|
29
|
+
}
|
|
30
|
+
export function buildReadyFunnelAttributionEventMetadata(attributionReady, attribution) {
|
|
31
|
+
return attributionReady ? buildFunnelAttributionEventMetadata(attribution) : null;
|
|
32
|
+
}
|
|
33
|
+
export function scheduleAttributionFailOpen(onReady, timeoutMs) {
|
|
34
|
+
const timeoutId = setTimeout(onReady, timeoutMs);
|
|
35
|
+
return () => clearTimeout(timeoutId);
|
|
36
|
+
}
|
|
37
|
+
export function takeInitialFunnelAttributionMetadata(tracked, attributionReady, attribution) {
|
|
38
|
+
if (tracked) {
|
|
39
|
+
return { tracked, metadata: null };
|
|
40
|
+
}
|
|
41
|
+
const metadata = buildReadyFunnelAttributionEventMetadata(attributionReady, attribution);
|
|
42
|
+
return { tracked: metadata !== null, metadata };
|
|
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
|
+
}
|
|
16
214
|
export function computeRenderSuspended(input) {
|
|
17
215
|
var _a;
|
|
18
|
-
if (input.isPreviewRuntime
|
|
216
|
+
if (input.isPreviewRuntime) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (input.attributionReady === false) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (input.postHogReady) {
|
|
19
223
|
return false;
|
|
20
224
|
}
|
|
21
225
|
const activeExperiment = input.experiments.find((experiment) => experiment.stepId === input.activeStepId);
|
|
@@ -65,15 +269,16 @@ export function resolveRenderedExperimentStepId(input) {
|
|
|
65
269
|
const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
|
|
66
270
|
return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
|
|
67
271
|
}
|
|
68
|
-
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, }) {
|
|
69
273
|
var _a, _b;
|
|
70
274
|
const [runtimeMode, setRuntimeMode] = useRuntimeMode();
|
|
71
|
-
const [editorModeEnabled, setEditorModeEnabled] = useState(
|
|
275
|
+
const [editorModeEnabled, setEditorModeEnabled] = useState(() => isEditorEnabled());
|
|
72
276
|
const [editorPanelExpanded, setEditorPanelExpanded] = useState(false);
|
|
73
277
|
const [postHogReady, setPostHogReady] = useState(() => isPostHogReady());
|
|
74
278
|
const [editorVariantOverrides, setEditorVariantOverrides] = useState({});
|
|
75
279
|
const shouldLockToInitialStep = lockToInitialStep || isPreviewStepLockRequested();
|
|
76
280
|
const isPreviewRuntime = useMemo(() => isPreviewFrameRuntime(), []);
|
|
281
|
+
const lifecycleEventsSuppressed = isPreviewRuntime || editorModeEnabled;
|
|
77
282
|
const resolveRenderableStepId = useCallback((stepId) => {
|
|
78
283
|
if (!stepId || !isFunnelStepId(stepId)) {
|
|
79
284
|
return null;
|
|
@@ -106,7 +311,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
106
311
|
const [activeStepId, setActiveStepId] = useState(safeInitialStepId);
|
|
107
312
|
const [attributes, setAttributes] = useState(() => (Object.assign({}, (initialAttributes || {}))));
|
|
108
313
|
const [user, setUser] = useState(() => ({
|
|
109
|
-
id:
|
|
314
|
+
id: api.getOrCreateClientUserId(),
|
|
110
315
|
name: '',
|
|
111
316
|
email: '',
|
|
112
317
|
attributes: {},
|
|
@@ -114,21 +319,35 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
114
319
|
completedSteps: [],
|
|
115
320
|
}));
|
|
116
321
|
const [userBootstrapped, setUserBootstrapped] = useState(isPreviewRuntime);
|
|
322
|
+
const [attributionReady, setAttributionReady] = useState(isPreviewRuntime);
|
|
117
323
|
const attributesAtStepStart = useRef({});
|
|
118
324
|
const attributesRef = useRef(attributes);
|
|
119
325
|
const postHogFeatureFlagsRef = useRef({});
|
|
120
326
|
const prevStepIdRef = useRef(null);
|
|
121
|
-
const
|
|
122
|
-
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);
|
|
123
332
|
const currentUserIdRef = useRef(user.id);
|
|
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);
|
|
124
342
|
const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
|
|
125
343
|
const activeStepMeta = stepById[safeActiveStepId];
|
|
126
344
|
const renderSuspended = useMemo(() => computeRenderSuspended({
|
|
127
345
|
activeStepId: safeActiveStepId,
|
|
346
|
+
attributionReady,
|
|
128
347
|
postHogReady,
|
|
129
348
|
experiments: funnelExperiments,
|
|
130
349
|
isPreviewRuntime,
|
|
131
|
-
}), [funnelExperiments, isPreviewRuntime, postHogReady, safeActiveStepId]);
|
|
350
|
+
}), [attributionReady, funnelExperiments, isPreviewRuntime, postHogReady, safeActiveStepId]);
|
|
132
351
|
const resolveExperimentVariantKey = useCallback((experiment) => {
|
|
133
352
|
var _a;
|
|
134
353
|
const override = editorVariantOverrides[experiment.experimentId];
|
|
@@ -157,6 +376,16 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
157
376
|
}), [activeExperimentForStep, editorVariantOverrides, postHogReady, resolveRenderableStepId, safeActiveStepId]);
|
|
158
377
|
const renderedStepMeta = stepById[renderedStepId];
|
|
159
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';
|
|
160
389
|
const postHogFeatureFlags = useMemo(() => {
|
|
161
390
|
if (!postHogReady || isPreviewRuntime) {
|
|
162
391
|
return {};
|
|
@@ -166,12 +395,20 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
166
395
|
return variantKey ? [[experiment.experimentId, variantKey]] : [];
|
|
167
396
|
}));
|
|
168
397
|
}, [funnelExperiments, isPreviewRuntime, postHogReady]);
|
|
169
|
-
const
|
|
170
|
-
var _a;
|
|
398
|
+
const resolveNavigationTarget = useCallback((stepId) => {
|
|
399
|
+
var _a, _b;
|
|
171
400
|
if (shouldLockToInitialStep) {
|
|
172
|
-
return;
|
|
401
|
+
return null;
|
|
173
402
|
}
|
|
174
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) => {
|
|
175
412
|
const stepPath = getPathForStep(safeStepId);
|
|
176
413
|
const nextLocation = buildHostedStepLocation({
|
|
177
414
|
currentHref: window.location.href,
|
|
@@ -188,16 +425,38 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
188
425
|
}
|
|
189
426
|
}
|
|
190
427
|
setActiveStepId(safeStepId);
|
|
191
|
-
}, [getPathForStep
|
|
428
|
+
}, [getPathForStep]);
|
|
192
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
|
+
}
|
|
193
441
|
setAttributes((prev) => (Object.assign(Object.assign({}, prev), { [key]: value })));
|
|
194
|
-
}, []);
|
|
442
|
+
}, [choiceGuardEnvironment, renderedStepId, reportChoiceDiagnostic, stepById, stepContractVersion]);
|
|
195
443
|
const getAnswer = useCallback((key) => {
|
|
196
444
|
return attributesRef.current[key];
|
|
197
445
|
}, []);
|
|
198
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
|
+
}
|
|
199
458
|
setAttributes((prev) => (Object.assign(Object.assign({}, prev), { [key]: value })));
|
|
200
|
-
}, []);
|
|
459
|
+
}, [choiceGuardEnvironment, renderedStepId, reportChoiceDiagnostic, stepById, stepContractVersion]);
|
|
201
460
|
useEffect(() => {
|
|
202
461
|
currentUserIdRef.current = user.id;
|
|
203
462
|
}, [user.id]);
|
|
@@ -224,9 +483,10 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
224
483
|
return () => window.clearInterval(intervalId);
|
|
225
484
|
}, [postHogReady]);
|
|
226
485
|
const resolveNextStepId = useCallback((stepId, options) => {
|
|
486
|
+
var _a;
|
|
227
487
|
return resolveNextStepFromContext({
|
|
228
488
|
stepId,
|
|
229
|
-
attributes: attributesRef.current,
|
|
489
|
+
attributes: (_a = options === null || options === void 0 ? void 0 : options.attributes) !== null && _a !== void 0 ? _a : attributesRef.current,
|
|
230
490
|
safeInitialStepId,
|
|
231
491
|
resolveRenderableStepId,
|
|
232
492
|
getConfiguredNextStepId: (currentStepId, context) => resolveConfiguredNextStep(currentStepId, context),
|
|
@@ -241,21 +501,18 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
241
501
|
resolveRenderableStepId,
|
|
242
502
|
safeInitialStepId,
|
|
243
503
|
]);
|
|
244
|
-
const goNext = useCallback(() => {
|
|
245
|
-
goToStep(resolveNextStepId(renderedStepId, {
|
|
246
|
-
ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
|
|
247
|
-
}));
|
|
248
|
-
}, [activeExperimentForStep, goToStep, renderedStepId, resolveNextStepId, safeActiveStepId]);
|
|
249
504
|
useEffect(() => {
|
|
250
|
-
const localUserId =
|
|
505
|
+
const localUserId = api.getOrCreateClientUserId();
|
|
251
506
|
const urlUserAttributes = resolveUrlUserAttributes();
|
|
252
|
-
const bootstrapCandidateUserId =
|
|
507
|
+
const bootstrapCandidateUserId = api.getBootstrapCandidateUserId(localUserId);
|
|
253
508
|
const initialAttribution = collectCurrentFunnelAttribution();
|
|
509
|
+
initialAttributionRef.current = initialAttribution;
|
|
254
510
|
setUserBootstrapped(isPreviewRuntime);
|
|
255
511
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId })));
|
|
256
512
|
if (isPreviewRuntime) {
|
|
257
513
|
return;
|
|
258
514
|
}
|
|
515
|
+
const cancelAttributionFailOpen = scheduleAttributionFailOpen(() => setAttributionReady(true), 5000);
|
|
259
516
|
const postHogBootstrap = bootstrapPostHog({
|
|
260
517
|
apiKey: POSTHOG_PROJECT_API_KEY,
|
|
261
518
|
apiHost: POSTHOG_API_HOST,
|
|
@@ -268,7 +525,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
268
525
|
logger.error('Failed to bootstrap PostHog feature flags:', error);
|
|
269
526
|
setPostHogReady(true);
|
|
270
527
|
});
|
|
271
|
-
const sessionBootstrap =
|
|
528
|
+
const sessionBootstrap = api
|
|
272
529
|
.bootstrapSession({
|
|
273
530
|
userId: bootstrapCandidateUserId || localUserId,
|
|
274
531
|
email: urlUserAttributes.email || undefined,
|
|
@@ -276,15 +533,18 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
276
533
|
attribution: initialAttribution,
|
|
277
534
|
})
|
|
278
535
|
.then((sessionUser) => {
|
|
536
|
+
cancelAttributionFailOpen();
|
|
279
537
|
const sessionAttributes = Object.keys(sessionUser.attributes || {}).length > 0
|
|
280
538
|
? sessionUser.attributes
|
|
281
539
|
: null;
|
|
282
540
|
if (sessionAttributes) {
|
|
283
541
|
setAttributes((prev) => (Object.assign(Object.assign({}, sessionAttributes), prev)));
|
|
284
542
|
}
|
|
543
|
+
initialAttributionRef.current = resolveFunnelEventAttribution(sessionUser.document, initialAttribution);
|
|
285
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 })));
|
|
286
545
|
setUserBootstrapped(true);
|
|
287
|
-
|
|
546
|
+
setAttributionReady(true);
|
|
547
|
+
void api.pingUserContext(sessionUser.id)
|
|
288
548
|
.then((pingedUser) => {
|
|
289
549
|
if (!pingedUser) {
|
|
290
550
|
return;
|
|
@@ -301,8 +561,10 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
301
561
|
return sessionUser;
|
|
302
562
|
})
|
|
303
563
|
.catch((error) => {
|
|
564
|
+
cancelAttributionFailOpen();
|
|
304
565
|
logger.error('Failed to bootstrap user session:', error);
|
|
305
566
|
setUserBootstrapped(false);
|
|
567
|
+
setAttributionReady(true);
|
|
306
568
|
return null;
|
|
307
569
|
});
|
|
308
570
|
Promise.all([postHogBootstrap, sessionBootstrap])
|
|
@@ -314,16 +576,21 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
314
576
|
.catch((error) => {
|
|
315
577
|
logger.error('Failed to identify PostHog user:', error);
|
|
316
578
|
});
|
|
317
|
-
|
|
318
|
-
|
|
579
|
+
return cancelAttributionFailOpen;
|
|
580
|
+
}, [api, isPreviewRuntime]);
|
|
581
|
+
const recordStepCompletion = useCallback((intent, options = {}) => {
|
|
319
582
|
var _a;
|
|
320
|
-
const meta = stepById[stepId];
|
|
321
|
-
if (!meta) {
|
|
583
|
+
const meta = stepById[intent.visit.stepId];
|
|
584
|
+
if (!meta || lifecycleEventsSuppressed) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
588
|
+
if (attributionMetadata === null) {
|
|
322
589
|
return;
|
|
323
590
|
}
|
|
324
591
|
let choices;
|
|
325
|
-
if (explicitChoices) {
|
|
326
|
-
choices = explicitChoices;
|
|
592
|
+
if (options.explicitChoices !== undefined) {
|
|
593
|
+
choices = options.explicitChoices;
|
|
327
594
|
}
|
|
328
595
|
else {
|
|
329
596
|
const snapshot = attributesAtStepStart.current;
|
|
@@ -341,28 +608,14 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
341
608
|
choices,
|
|
342
609
|
};
|
|
343
610
|
const stepType = meta.type;
|
|
344
|
-
const
|
|
611
|
+
const completionAttributes = Object.assign({}, attributesRef.current);
|
|
345
612
|
dispatchWindowCustomEvent('funnel:step-completed', record);
|
|
346
|
-
if (!isPreviewRuntime) {
|
|
347
|
-
const analyticsRuntimeMode = getRuntimeMode();
|
|
348
|
-
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepCompleted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
349
|
-
userId: currentUserIdRef.current,
|
|
350
|
-
environment: analyticsRuntimeMode,
|
|
351
|
-
stepId: record.stepId,
|
|
352
|
-
stepName: record.stepName,
|
|
353
|
-
stepType,
|
|
354
|
-
startedAt,
|
|
355
|
-
endedAt: record.completedAt,
|
|
356
|
-
selected: record.choices,
|
|
357
|
-
featureFlags: postHogFeatureFlagsRef.current,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
613
|
setUser((prev) => {
|
|
361
614
|
var _a, _b;
|
|
362
615
|
const updatedSteps = [...((_a = prev.completedSteps) !== null && _a !== void 0 ? _a : []), record];
|
|
363
|
-
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 });
|
|
364
617
|
if (!isPreviewRuntime) {
|
|
365
|
-
|
|
618
|
+
api
|
|
366
619
|
.updateUser({
|
|
367
620
|
id: updatedUser.id,
|
|
368
621
|
name: updatedUser.name,
|
|
@@ -378,14 +631,347 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
378
631
|
}
|
|
379
632
|
return updatedUser;
|
|
380
633
|
});
|
|
381
|
-
|
|
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]);
|
|
382
830
|
const completeStep = useCallback((stepId, choices) => {
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
]);
|
|
385
970
|
useEffect(() => {
|
|
386
971
|
var _a, _b, _c;
|
|
387
972
|
if (safeActiveStepId !== activeStepId) {
|
|
388
973
|
logger.warn(`[FunnelFlow] Unknown or non-renderable step "${activeStepId}", falling back to "${safeActiveStepId}".`);
|
|
974
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
389
975
|
setActiveStepId(safeActiveStepId);
|
|
390
976
|
return;
|
|
391
977
|
}
|
|
@@ -397,14 +983,32 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
397
983
|
return;
|
|
398
984
|
}
|
|
399
985
|
if (prevId && prevId !== renderedStepId) {
|
|
400
|
-
|
|
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
|
+
}
|
|
401
992
|
}
|
|
402
993
|
const startedAt = new Date().toISOString();
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
994
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
995
|
+
if (attributionMetadata === null || !renderedStepMeta) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
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;
|
|
406
1008
|
const analyticsRuntimeMode = getRuntimeMode();
|
|
407
|
-
|
|
1009
|
+
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
1010
|
+
prevStepIdRef.current = renderedStepId;
|
|
1011
|
+
if (!lifecycleEventsSuppressed) {
|
|
408
1012
|
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepStarted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
409
1013
|
userId: currentUserIdRef.current,
|
|
410
1014
|
environment: analyticsRuntimeMode,
|
|
@@ -412,48 +1016,122 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
412
1016
|
stepName,
|
|
413
1017
|
stepType,
|
|
414
1018
|
startedAt,
|
|
1019
|
+
stepContractVersion,
|
|
1020
|
+
metadata: Object.assign(Object.assign({}, attributionMetadata), { step_contract_version: stepContractVersion }),
|
|
415
1021
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
416
1022
|
});
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
});
|
|
1038
|
+
}
|
|
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, {
|
|
429
1096
|
userId: currentUserIdRef.current,
|
|
430
|
-
environment:
|
|
1097
|
+
environment: getRuntimeMode(),
|
|
431
1098
|
stepId: renderedStepId,
|
|
432
|
-
stepName,
|
|
433
|
-
stepType,
|
|
434
|
-
|
|
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 }),
|
|
435
1104
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
436
1105
|
});
|
|
437
1106
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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);
|
|
441
1113
|
}, [
|
|
442
|
-
activeStepId,
|
|
443
1114
|
analytics,
|
|
1115
|
+
attributionReady,
|
|
444
1116
|
defaultStepId,
|
|
445
|
-
|
|
446
|
-
recordStepCompletion,
|
|
1117
|
+
lifecycleEventsSuppressed,
|
|
447
1118
|
renderSuspended,
|
|
448
1119
|
renderedStepId,
|
|
449
|
-
renderedStepMeta
|
|
450
|
-
renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.title,
|
|
451
|
-
renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.type,
|
|
1120
|
+
renderedStepMeta,
|
|
452
1121
|
safeActiveStepId,
|
|
1122
|
+
stepContractVersion,
|
|
453
1123
|
]);
|
|
454
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' });
|
|
455
1133
|
setActiveStepId(safeInitialStepId);
|
|
456
|
-
}, [safeInitialStepId]);
|
|
1134
|
+
}, [claimCurrentVisit, safeActiveStepId, safeInitialStepId]);
|
|
457
1135
|
useEffect(() => {
|
|
458
1136
|
setEditorModeEnabled(isEditorEnabled());
|
|
459
1137
|
}, [safeActiveStepId]);
|
|
@@ -464,18 +1142,34 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
464
1142
|
}, [editorModeEnabled]);
|
|
465
1143
|
useEffect(() => {
|
|
466
1144
|
if (shouldLockToInitialStep) {
|
|
467
|
-
|
|
1145
|
+
if (safeActiveStepId !== safeInitialStepId) {
|
|
1146
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
1147
|
+
setActiveStepId(safeInitialStepId);
|
|
1148
|
+
}
|
|
468
1149
|
return;
|
|
469
1150
|
}
|
|
470
1151
|
const syncStepWithPath = () => {
|
|
471
1152
|
var _a;
|
|
472
1153
|
const resolvedPathStep = getStepIdFromPath(window.location.pathname);
|
|
473
1154
|
const resolvedStep = (_a = resolveRenderableStepId(resolvedPathStep)) !== null && _a !== void 0 ? _a : safeInitialStepId;
|
|
474
|
-
|
|
1155
|
+
const targetStepId = resolveNavigationTarget(resolvedStep);
|
|
1156
|
+
if (!targetStepId) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
claimCurrentVisit({ type: 'exit', reason: 'back' });
|
|
1160
|
+
setActiveStepId(targetStepId);
|
|
475
1161
|
};
|
|
476
1162
|
window.addEventListener('popstate', syncStepWithPath);
|
|
477
1163
|
return () => window.removeEventListener('popstate', syncStepWithPath);
|
|
478
|
-
}, [
|
|
1164
|
+
}, [
|
|
1165
|
+
claimCurrentVisit,
|
|
1166
|
+
getStepIdFromPath,
|
|
1167
|
+
resolveNavigationTarget,
|
|
1168
|
+
resolveRenderableStepId,
|
|
1169
|
+
safeActiveStepId,
|
|
1170
|
+
safeInitialStepId,
|
|
1171
|
+
shouldLockToInitialStep,
|
|
1172
|
+
]);
|
|
479
1173
|
const nextStepId = useMemo(() => resolveNextStepFromContext({
|
|
480
1174
|
stepId: renderedStepId,
|
|
481
1175
|
attributes,
|
|
@@ -511,7 +1205,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
511
1205
|
ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
|
|
512
1206
|
});
|
|
513
1207
|
const timer = window.setTimeout(() => {
|
|
514
|
-
goToStep(autoAdvanceTarget);
|
|
1208
|
+
goToStep(autoAdvanceTarget, { type: 'complete' });
|
|
515
1209
|
}, autoAdvanceMs);
|
|
516
1210
|
return () => window.clearTimeout(timer);
|
|
517
1211
|
}, [
|
|
@@ -525,25 +1219,53 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
525
1219
|
shouldLockToInitialStep,
|
|
526
1220
|
]);
|
|
527
1221
|
const handleContinue = useCallback(() => {
|
|
528
|
-
goToStep(nextStepId);
|
|
1222
|
+
goToStep(nextStepId, { type: 'complete' });
|
|
529
1223
|
}, [goToStep, nextStepId]);
|
|
530
|
-
const goToStepFromContext = useCallback((stepId) => {
|
|
1224
|
+
const goToStepFromContext = useCallback((stepId, outcome) => {
|
|
531
1225
|
if (!isFunnelStepId(stepId)) {
|
|
532
1226
|
return;
|
|
533
1227
|
}
|
|
534
|
-
goToStep(stepId);
|
|
1228
|
+
goToStep(stepId, outcome);
|
|
535
1229
|
}, [goToStep, isFunnelStepId]);
|
|
536
1230
|
const goChoice = useCallback((choice, stepId) => {
|
|
537
1231
|
var _a, _b;
|
|
538
1232
|
const sourceStepId = (_a = resolveRenderableStepId(stepId)) !== null && _a !== void 0 ? _a : renderedStepId;
|
|
539
|
-
|
|
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);
|
|
540
1248
|
const sourceChoiceTargets = getChoiceTargetsForStep(sourceStepId);
|
|
541
1249
|
const choiceTarget = (_b = resolveRenderableStepId(sourceChoiceTargets === null || sourceChoiceTargets === void 0 ? void 0 : sourceChoiceTargets[choice])) !== null && _b !== void 0 ? _b : resolveNextStepId(sourceStepId);
|
|
542
|
-
goToStep(choiceTarget);
|
|
543
|
-
}, [
|
|
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
|
+
]);
|
|
544
1262
|
usePreviewBridge({
|
|
545
1263
|
activeStepId: safeActiveStepId,
|
|
546
|
-
onGoToStep:
|
|
1264
|
+
onGoToStep: (stepId) => {
|
|
1265
|
+
if (isFunnelStepId(stepId)) {
|
|
1266
|
+
goToStep(stepId, { type: 'exit', reason: 'replace' });
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
547
1269
|
resolveRenderableStepId,
|
|
548
1270
|
setRuntimeMode,
|
|
549
1271
|
shouldLockToInitialStep,
|
|
@@ -554,11 +1276,12 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
554
1276
|
return;
|
|
555
1277
|
}
|
|
556
1278
|
const selectedVariant = experiment.variants.find((variant) => variant.variantKey === nextVariantKey);
|
|
557
|
-
if (!selectedVariant) {
|
|
1279
|
+
if (!selectedVariant || selectedVariant.routeToStepId === renderedStepId) {
|
|
558
1280
|
return;
|
|
559
1281
|
}
|
|
1282
|
+
claimCurrentVisit({ type: 'exit', reason: 'replace' });
|
|
560
1283
|
setEditorVariantOverrides((prev) => (Object.assign(Object.assign({}, prev), { [experiment.experimentId]: selectedVariant.variantKey })));
|
|
561
|
-
}, []);
|
|
1284
|
+
}, [claimCurrentVisit, renderedStepId]);
|
|
562
1285
|
const experimentAssignmentForEditor = useCallback((experiment) => {
|
|
563
1286
|
var _a;
|
|
564
1287
|
const override = editorVariantOverrides[experiment.experimentId];
|
|
@@ -583,30 +1306,40 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
583
1306
|
}
|
|
584
1307
|
return ((_a = experiment.variants[0]) === null || _a === void 0 ? void 0 : _a.variantKey) || '';
|
|
585
1308
|
}, [editorVariantOverrides, postHogReady]);
|
|
586
|
-
const contextValue = useMemo(() =>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
+
}, [
|
|
608
1339
|
attributes,
|
|
609
1340
|
completeStep,
|
|
1341
|
+
completeFunnel,
|
|
1342
|
+
commitChoiceCompletion,
|
|
610
1343
|
getAnswer,
|
|
611
1344
|
getChoiceTargetsForStep,
|
|
612
1345
|
goChoice,
|
|
@@ -618,6 +1351,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
618
1351
|
resolveRenderableStepId,
|
|
619
1352
|
setAnswer,
|
|
620
1353
|
setAttribute,
|
|
1354
|
+
submitEmailCapture,
|
|
621
1355
|
user,
|
|
622
1356
|
userBootstrapped,
|
|
623
1357
|
]);
|