@funnelsgrove/runtime 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/components/FunnelContext.d.ts +5 -2
- package/dist/components/FunnelContext.js +3 -0
- package/dist/components/FunnelEditorPanel.d.ts +3 -5
- package/dist/components/FunnelEditorPanel.js +3 -3
- package/dist/components/ManageSubscriptionScreen.d.ts +51 -0
- package/dist/components/ManageSubscriptionScreen.js +349 -0
- package/dist/components/RuntimeDevInfoBox.d.ts +23 -0
- package/dist/components/RuntimeDevInfoBox.js +363 -0
- package/dist/components/SubscriptionHandoffScreen.d.ts +31 -0
- package/dist/components/SubscriptionHandoffScreen.js +338 -0
- package/dist/config/builder-preview.protocol.d.ts +73 -0
- package/dist/config/builder-preview.protocol.js +3 -0
- package/dist/config/env.config.d.ts +44 -0
- package/dist/config/env.config.js +161 -0
- package/dist/config/font-config.d.ts +14 -0
- package/dist/config/font-config.js +101 -0
- package/dist/config/funnel-theme.d.ts +61 -10
- package/dist/config/funnel-theme.js +355 -35
- package/dist/config/funnel.manifest.types.d.ts +13 -7
- package/dist/content/step-content.d.ts +130 -0
- package/dist/content/step-content.js +381 -0
- package/dist/index.d.ts +33 -21
- package/dist/index.js +33 -21
- package/dist/runtime/browser-helpers.d.ts +1 -0
- package/dist/runtime/browser-helpers.js +14 -0
- package/dist/runtime/experiment-assignment.d.ts +13 -4
- package/dist/runtime/experiment-assignment.js +9 -27
- package/dist/runtime/funnel-attribution.d.ts +18 -0
- package/dist/runtime/funnel-attribution.js +226 -0
- package/dist/runtime/funnel-flow.d.ts +9 -10
- package/dist/runtime/funnel-flow.js +4 -18
- package/dist/runtime/funnel-manifest.validation.d.ts +1 -1
- package/dist/runtime/funnel-manifest.validation.js +2 -6
- package/dist/runtime/funnel-runtime.d.ts +2 -3
- package/dist/runtime/funnel-runtime.js +6 -13
- package/dist/runtime/posthog-flags.d.ts +30 -0
- package/dist/runtime/posthog-flags.js +71 -0
- package/dist/runtime/preview-bridge.d.ts +13 -3
- package/dist/runtime/preview-bridge.js +96 -4
- package/dist/runtime/preview-definition-overrides.d.ts +20 -0
- package/dist/runtime/preview-definition-overrides.js +148 -0
- package/dist/runtime/route-resolver.d.ts +2 -3
- package/dist/runtime/route-resolver.js +15 -26
- package/dist/runtime/subscription-handoff.d.ts +32 -0
- package/dist/runtime/subscription-handoff.js +113 -0
- package/dist/runtime/use-funnel-flow-controller.d.ts +19 -10
- package/dist/runtime/use-funnel-flow-controller.js +190 -159
- package/dist/sdk/userAnswers.d.ts +2 -2
- package/dist/services/api.service.d.ts +21 -4
- package/dist/services/api.service.js +165 -35
- package/dist/services/funnel-state.service.d.ts +8 -0
- package/dist/services/funnel-state.service.js +44 -0
- package/dist/services/preview-frame.service.d.ts +2 -2
- package/dist/services/preview-frame.service.js +2 -2
- package/dist/services/public-env.d.ts +69 -0
- package/dist/services/public-env.js +105 -0
- package/dist/services/runtime-api.config.d.ts +5 -0
- package/dist/services/runtime-api.config.js +12 -7
- package/dist/services/runtime-mode.service.d.ts +3 -0
- package/dist/services/runtime-mode.service.js +142 -4
- package/package.json +8 -2
|
@@ -1,25 +1,69 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { writeStepChoice } from '../sdk/userAnswers';
|
|
4
|
-
import { apiService } from '../services/api.service';
|
|
5
|
-
import { logger } from '../services/logger';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
3
|
+
import { writeStepChoice } from '../sdk/userAnswers.js';
|
|
4
|
+
import { apiService } from '../services/api.service.js';
|
|
5
|
+
import { logger } from '../services/logger.js';
|
|
6
|
+
import { FUNNEL_ID, FUNNEL_VERSION_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from '../services/runtime-api.config.js';
|
|
7
|
+
import { buildHostedStepLocation, dispatchWindowCustomEvent, } from './browser-helpers.js';
|
|
8
|
+
import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAdvanceTimer, } from './funnel-flow.js';
|
|
9
|
+
import { usePreviewBridge } from './preview-bridge.js';
|
|
10
|
+
import { resolveExperimentAssignment } from './experiment-assignment.js';
|
|
11
|
+
import { collectCurrentFunnelAttribution } from './funnel-attribution.js';
|
|
12
|
+
import { bootstrapPostHog, getPostHog, identifyPostHog, isPostHogReady, resolveExperimentVariant, } from './posthog-flags.js';
|
|
13
|
+
import { isEditorEnabled, useRuntimeMode } from '../services/runtime-mode.service.js';
|
|
14
|
+
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
15
|
+
const buildFeatureFlagProperties = (featureFlags) => {
|
|
16
|
+
return Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [`$feature/${key}`, value]));
|
|
17
|
+
};
|
|
18
|
+
const capturePostHogStepEvent = (event, properties) => {
|
|
19
|
+
if (!getPostHog() || !POSTHOG_API_HOST || !POSTHOG_PROJECT_API_KEY || !PROJECT_ID) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const payload = JSON.stringify({
|
|
23
|
+
api_key: POSTHOG_PROJECT_API_KEY,
|
|
24
|
+
batch: [
|
|
25
|
+
{
|
|
26
|
+
event,
|
|
27
|
+
properties: Object.assign({ $groups: {
|
|
28
|
+
project: PROJECT_ID,
|
|
29
|
+
}, $insert_id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
30
|
+
? crypto.randomUUID()
|
|
31
|
+
: `${event}_${Date.now()}_${Math.random().toString(16).slice(2)}`, event_source: 'client' }, properties),
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
const endpoint = `${POSTHOG_API_HOST}/batch/`;
|
|
37
|
+
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
|
38
|
+
const accepted = navigator.sendBeacon(endpoint, new Blob([payload], { type: 'application/json' }));
|
|
39
|
+
if (accepted) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
void fetch(endpoint, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: payload,
|
|
47
|
+
keepalive: true,
|
|
48
|
+
}).catch(() => {
|
|
49
|
+
// Analytics delivery must never block funnel interaction.
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
export function computeRenderSuspended(input) {
|
|
53
|
+
if (input.isPreviewRuntime || input.postHogReady) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return input.experiments.some((experiment) => experiment.stepId === input.activeStepId);
|
|
57
|
+
}
|
|
12
58
|
export function useFunnelFlowController({ initialStepId, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
|
|
13
59
|
var _a, _b;
|
|
14
60
|
const [runtimeMode, setRuntimeMode] = useRuntimeMode();
|
|
15
61
|
const [editorModeEnabled, setEditorModeEnabled] = useState(false);
|
|
16
62
|
const [editorPanelExpanded, setEditorPanelExpanded] = useState(false);
|
|
63
|
+
const [postHogReady, setPostHogReady] = useState(() => isPostHogReady());
|
|
64
|
+
const [editorVariantOverrides, setEditorVariantOverrides] = useState({});
|
|
17
65
|
const shouldLockToInitialStep = lockToInitialStep || isPreviewStepLockRequested();
|
|
18
66
|
const isPreviewRuntime = useMemo(() => isPreviewFrameRuntime(), []);
|
|
19
|
-
const experimentAssignmentStorage = useMemo(() => ({
|
|
20
|
-
read: readWindowStorageValue,
|
|
21
|
-
write: writeWindowStorageValue,
|
|
22
|
-
}), []);
|
|
23
67
|
const resolveRenderableStepId = useCallback((stepId) => {
|
|
24
68
|
if (!stepId || !isFunnelStepId(stepId)) {
|
|
25
69
|
return null;
|
|
@@ -46,7 +90,6 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
46
90
|
const safeInitialStepId = useMemo(() => {
|
|
47
91
|
const requestedStepId = resolveRuntimeInitialStepId({
|
|
48
92
|
requestedStepId: initialStepId,
|
|
49
|
-
entryPointId: getRequestedEntryPointId(),
|
|
50
93
|
});
|
|
51
94
|
return resolveFallbackStepId(requestedStepId);
|
|
52
95
|
}, [initialStepId, resolveFallbackStepId, resolveRuntimeInitialStepId]);
|
|
@@ -57,30 +100,69 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
57
100
|
name: '',
|
|
58
101
|
email: '',
|
|
59
102
|
attributes: {},
|
|
103
|
+
document: {},
|
|
60
104
|
completedSteps: [],
|
|
61
105
|
}));
|
|
62
106
|
const attributesAtStepStart = useRef({});
|
|
63
107
|
const attributesRef = useRef(attributes);
|
|
108
|
+
const postHogFeatureFlagsRef = useRef({});
|
|
64
109
|
const prevStepIdRef = useRef(null);
|
|
65
110
|
const stepStartedAtByIdRef = useRef({});
|
|
66
111
|
const currentUserIdRef = useRef(user.id);
|
|
67
112
|
const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
|
|
68
113
|
const activeStepMeta = stepById[safeActiveStepId];
|
|
114
|
+
const renderSuspended = useMemo(() => computeRenderSuspended({
|
|
115
|
+
activeStepId: safeActiveStepId,
|
|
116
|
+
postHogReady,
|
|
117
|
+
experiments: funnelExperiments,
|
|
118
|
+
isPreviewRuntime,
|
|
119
|
+
}), [funnelExperiments, isPreviewRuntime, postHogReady, safeActiveStepId]);
|
|
120
|
+
const resolveExperimentVariantKey = useCallback((experiment) => {
|
|
121
|
+
var _a;
|
|
122
|
+
const override = editorVariantOverrides[experiment.experimentId];
|
|
123
|
+
if (override) {
|
|
124
|
+
return override;
|
|
125
|
+
}
|
|
126
|
+
if (!postHogReady) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return (_a = resolveExperimentVariant(experiment.experimentId)) !== null && _a !== void 0 ? _a : null;
|
|
130
|
+
}, [editorVariantOverrides, postHogReady]);
|
|
69
131
|
const renderedStepId = useMemo(() => {
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
132
|
+
const experiment = funnelExperiments.find((candidate) => candidate.stepId === safeActiveStepId);
|
|
133
|
+
if (!experiment) {
|
|
72
134
|
return safeActiveStepId;
|
|
73
135
|
}
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
136
|
+
const override = editorVariantOverrides[experiment.experimentId];
|
|
137
|
+
if (override) {
|
|
138
|
+
const matchedVariant = experiment.variants.find((variant) => variant.variantKey === override);
|
|
139
|
+
const variantStepId = resolveRenderableStepId((matchedVariant === null || matchedVariant === void 0 ? void 0 : matchedVariant.routeToStepId) || null);
|
|
140
|
+
return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
|
|
141
|
+
}
|
|
142
|
+
if (!postHogReady) {
|
|
76
143
|
return safeActiveStepId;
|
|
77
144
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
145
|
+
const assignment = resolveExperimentAssignment({
|
|
146
|
+
experimentId: experiment.experimentId,
|
|
147
|
+
variants: experiment.variants,
|
|
148
|
+
});
|
|
149
|
+
if (!assignment) {
|
|
150
|
+
return safeActiveStepId;
|
|
151
|
+
}
|
|
152
|
+
const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
|
|
80
153
|
return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
|
|
81
|
-
}, [
|
|
154
|
+
}, [editorVariantOverrides, funnelExperiments, postHogReady, resolveRenderableStepId, safeActiveStepId]);
|
|
82
155
|
const renderedStepMeta = stepById[renderedStepId];
|
|
83
156
|
const activeStepComponent = stepComponentById[renderedStepId];
|
|
157
|
+
const postHogFeatureFlags = useMemo(() => {
|
|
158
|
+
if (!postHogReady || isPreviewRuntime) {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
return Object.fromEntries(funnelExperiments.flatMap((experiment) => {
|
|
162
|
+
const variantKey = resolveExperimentVariant(experiment.experimentId);
|
|
163
|
+
return variantKey ? [[experiment.experimentId, variantKey]] : [];
|
|
164
|
+
}));
|
|
165
|
+
}, [funnelExperiments, isPreviewRuntime, postHogReady]);
|
|
84
166
|
const goToStep = useCallback((stepId) => {
|
|
85
167
|
var _a;
|
|
86
168
|
if (shouldLockToInitialStep) {
|
|
@@ -119,38 +201,67 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
119
201
|
useEffect(() => {
|
|
120
202
|
attributesRef.current = attributes;
|
|
121
203
|
}, [attributes]);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
postHogFeatureFlagsRef.current = postHogFeatureFlags;
|
|
206
|
+
}, [postHogFeatureFlags]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (postHogReady) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (isPostHogReady()) {
|
|
212
|
+
setPostHogReady(true);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const intervalId = window.setInterval(() => {
|
|
216
|
+
if (isPostHogReady()) {
|
|
217
|
+
setPostHogReady(true);
|
|
218
|
+
window.clearInterval(intervalId);
|
|
219
|
+
}
|
|
220
|
+
}, 100);
|
|
221
|
+
return () => window.clearInterval(intervalId);
|
|
222
|
+
}, [postHogReady]);
|
|
122
223
|
const resolveNextStepId = useCallback((stepId) => {
|
|
123
|
-
const activeUserId = currentUserIdRef.current || user.id;
|
|
124
224
|
return resolveNextStepFromContext({
|
|
125
225
|
stepId,
|
|
126
226
|
attributes: attributesRef.current,
|
|
127
|
-
userId: activeUserId,
|
|
128
227
|
safeInitialStepId,
|
|
129
228
|
resolveRenderableStepId,
|
|
130
229
|
getConfiguredNextStepId: (currentStepId, context) => resolveConfiguredNextStep(currentStepId, context),
|
|
131
230
|
getSequentialNextStepId,
|
|
132
|
-
|
|
231
|
+
resolveExperimentVariantKey,
|
|
133
232
|
});
|
|
134
233
|
}, [
|
|
135
|
-
experimentAssignmentStorage,
|
|
136
234
|
getSequentialNextStepId,
|
|
137
235
|
resolveConfiguredNextStep,
|
|
236
|
+
resolveExperimentVariantKey,
|
|
138
237
|
resolveRenderableStepId,
|
|
139
238
|
safeInitialStepId,
|
|
140
|
-
user.id,
|
|
141
239
|
]);
|
|
142
240
|
const goNext = useCallback(() => {
|
|
143
241
|
goToStep(resolveNextStepId(safeActiveStepId));
|
|
144
242
|
}, [goToStep, resolveNextStepId, safeActiveStepId]);
|
|
145
243
|
useEffect(() => {
|
|
146
244
|
const localUserId = apiService.getOrCreateClientUserId();
|
|
245
|
+
const bootstrapCandidateUserId = apiService.getBootstrapCandidateUserId(localUserId);
|
|
246
|
+
const initialAttribution = collectCurrentFunnelAttribution();
|
|
147
247
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId })));
|
|
148
248
|
if (isPreviewRuntime) {
|
|
149
249
|
return;
|
|
150
250
|
}
|
|
151
|
-
|
|
251
|
+
const postHogBootstrap = bootstrapPostHog({
|
|
252
|
+
apiKey: POSTHOG_PROJECT_API_KEY,
|
|
253
|
+
apiHost: POSTHOG_API_HOST,
|
|
254
|
+
distinctId: localUserId,
|
|
255
|
+
})
|
|
256
|
+
.then(() => setPostHogReady(true))
|
|
257
|
+
.catch((error) => {
|
|
258
|
+
logger.error('Failed to bootstrap PostHog feature flags:', error);
|
|
259
|
+
setPostHogReady(true);
|
|
260
|
+
});
|
|
261
|
+
const sessionBootstrap = apiService
|
|
152
262
|
.bootstrapSession({
|
|
153
|
-
userId: localUserId,
|
|
263
|
+
userId: bootstrapCandidateUserId || localUserId,
|
|
264
|
+
attribution: initialAttribution,
|
|
154
265
|
})
|
|
155
266
|
.then((sessionUser) => {
|
|
156
267
|
const sessionAttributes = Object.keys(sessionUser.attributes || {}).length > 0
|
|
@@ -159,70 +270,23 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
159
270
|
if (sessionAttributes) {
|
|
160
271
|
setAttributes((prev) => (Object.assign(Object.assign({}, sessionAttributes), prev)));
|
|
161
272
|
}
|
|
162
|
-
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: sessionUser.id || prev.id, name: sessionUser.name || prev.name, email: sessionUser.email || prev.email, attributes: sessionAttributes ? Object.assign(Object.assign({}, sessionAttributes), prev.attributes) : prev.attributes })));
|
|
273
|
+
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 })));
|
|
274
|
+
return sessionUser;
|
|
163
275
|
})
|
|
164
276
|
.catch((error) => {
|
|
165
277
|
logger.error('Failed to bootstrap user session:', error);
|
|
278
|
+
return null;
|
|
166
279
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
const assignmentUpdates = {};
|
|
173
|
-
const assignmentEvents = [];
|
|
174
|
-
for (const experiment of funnelExperiments) {
|
|
175
|
-
if (experiment.status !== 'active') {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const assignedVariantId = getAssignedVariantId(experiment, currentUserIdRef.current, experimentAssignmentStorage);
|
|
179
|
-
if (!assignedVariantId) {
|
|
180
|
-
continue;
|
|
280
|
+
Promise.all([postHogBootstrap, sessionBootstrap])
|
|
281
|
+
.then(([, sessionUser]) => {
|
|
282
|
+
if (sessionUser === null || sessionUser === void 0 ? void 0 : sessionUser.id) {
|
|
283
|
+
identifyPostHog(sessionUser.id, localUserId);
|
|
181
284
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
assignmentUpdates[assignmentKey] = assignedVariantId;
|
|
187
|
-
const assignedVariant = experiment.variants.find((variant) => variant.id === assignedVariantId);
|
|
188
|
-
assignmentEvents.push({
|
|
189
|
-
experiment,
|
|
190
|
-
variantId: assignedVariantId,
|
|
191
|
-
variantLabel: (assignedVariant === null || assignedVariant === void 0 ? void 0 : assignedVariant.label) || assignedVariantId,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
const entries = Object.entries(assignmentUpdates);
|
|
195
|
-
if (entries.length === 0) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
setAttributes((prev) => {
|
|
199
|
-
let changed = false;
|
|
200
|
-
const next = Object.assign({}, prev);
|
|
201
|
-
for (const [key, value] of entries) {
|
|
202
|
-
if (next[key] !== value) {
|
|
203
|
-
next[key] = value;
|
|
204
|
-
changed = true;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return changed ? next : prev;
|
|
285
|
+
})
|
|
286
|
+
.catch((error) => {
|
|
287
|
+
logger.error('Failed to identify PostHog user:', error);
|
|
208
288
|
});
|
|
209
|
-
|
|
210
|
-
assignmentEvents.forEach(({ experiment, variantId, variantLabel }) => {
|
|
211
|
-
const stepMeta = stepById[experiment.stepId];
|
|
212
|
-
apiService.trackFunnelEvent({
|
|
213
|
-
userId: currentUserIdRef.current,
|
|
214
|
-
eventType: 'experiment_variant_assigned',
|
|
215
|
-
stepId: experiment.stepId,
|
|
216
|
-
stepName: (stepMeta === null || stepMeta === void 0 ? void 0 : stepMeta.name) || (stepMeta === null || stepMeta === void 0 ? void 0 : stepMeta.title) || experiment.stepId,
|
|
217
|
-
selected: {
|
|
218
|
-
experimentId: experiment.experimentId,
|
|
219
|
-
variantId,
|
|
220
|
-
variantLabel,
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}, [experimentAssignmentStorage, funnelExperiments, isPreviewRuntime, stepById]);
|
|
289
|
+
}, [isPreviewRuntime]);
|
|
226
290
|
const recordStepCompletion = useCallback((stepId, explicitChoices) => {
|
|
227
291
|
const meta = stepById[stepId];
|
|
228
292
|
if (!meta) {
|
|
@@ -258,11 +322,12 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
258
322
|
endedAt: record.completedAt,
|
|
259
323
|
selected: record.choices,
|
|
260
324
|
});
|
|
325
|
+
capturePostHogStepEvent('step_completed', Object.assign({ distinct_id: currentUserIdRef.current, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, project_id: PROJECT_ID || undefined, step_id: record.stepId, step_name: record.stepName, started_at: startedAt, ended_at: record.completedAt, selected: record.choices }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
261
326
|
}
|
|
262
327
|
setUser((prev) => {
|
|
263
|
-
var _a;
|
|
328
|
+
var _a, _b;
|
|
264
329
|
const updatedSteps = [...((_a = prev.completedSteps) !== null && _a !== void 0 ? _a : []), record];
|
|
265
|
-
const updatedUser = Object.assign(Object.assign({}, prev), { attributes: Object.assign(Object.assign({}, prev.attributes), attributesRef.current), completedSteps: updatedSteps });
|
|
330
|
+
const updatedUser = Object.assign(Object.assign({}, prev), { attributes: Object.assign(Object.assign({}, prev.attributes), attributesRef.current), document: prev.document, completedSteps: updatedSteps });
|
|
266
331
|
if (!isPreviewRuntime) {
|
|
267
332
|
apiService
|
|
268
333
|
.updateUser({
|
|
@@ -270,6 +335,9 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
270
335
|
name: updatedUser.name,
|
|
271
336
|
email: updatedUser.email,
|
|
272
337
|
attributes: Object.assign(Object.assign({}, updatedUser.attributes), { completedSteps: updatedSteps }),
|
|
338
|
+
document: (_b = updatedUser.document) !== null && _b !== void 0 ? _b : {},
|
|
339
|
+
}, {
|
|
340
|
+
attribution: collectCurrentFunnelAttribution(),
|
|
273
341
|
})
|
|
274
342
|
.catch((error) => {
|
|
275
343
|
logger.error('Failed to persist step completion:', error);
|
|
@@ -287,6 +355,9 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
287
355
|
setActiveStepId(safeActiveStepId);
|
|
288
356
|
return;
|
|
289
357
|
}
|
|
358
|
+
if (renderSuspended) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
290
361
|
const prevId = prevStepIdRef.current;
|
|
291
362
|
if (prevId && prevId !== safeActiveStepId) {
|
|
292
363
|
recordStepCompletion(prevId);
|
|
@@ -300,28 +371,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
300
371
|
stepName: (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.name) || (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.title) || safeActiveStepId,
|
|
301
372
|
startedAt,
|
|
302
373
|
});
|
|
303
|
-
|
|
304
|
-
const stepExperiments = funnelExperiments.filter((experiment) => experiment.status === 'active' && experiment.stepId === safeActiveStepId);
|
|
305
|
-
for (const experiment of stepExperiments) {
|
|
306
|
-
const assignedVariantId = getAssignedVariantId(experiment, currentUserIdRef.current, experimentAssignmentStorage);
|
|
307
|
-
if (!assignedVariantId) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const assignedVariant = experiment.variants.find((variant) => variant.id === assignedVariantId);
|
|
311
|
-
if (!isPreviewRuntime) {
|
|
312
|
-
apiService.trackFunnelEvent({
|
|
313
|
-
userId: currentUserIdRef.current,
|
|
314
|
-
eventType: 'experiment_step_exposed',
|
|
315
|
-
stepId: safeActiveStepId,
|
|
316
|
-
stepName: (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.name) || (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.title) || safeActiveStepId,
|
|
317
|
-
startedAt,
|
|
318
|
-
selected: {
|
|
319
|
-
experimentId: experiment.experimentId,
|
|
320
|
-
variantId: assignedVariantId,
|
|
321
|
-
variantLabel: (assignedVariant === null || assignedVariant === void 0 ? void 0 : assignedVariant.label) || assignedVariantId,
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
}
|
|
374
|
+
capturePostHogStepEvent('step_started', Object.assign({ distinct_id: currentUserIdRef.current, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, project_id: PROJECT_ID || undefined, step_id: safeActiveStepId, step_name: (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.name) || (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.title) || safeActiveStepId, started_at: startedAt }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
325
375
|
}
|
|
326
376
|
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
327
377
|
prevStepIdRef.current = safeActiveStepId;
|
|
@@ -329,10 +379,9 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
329
379
|
activeStepId,
|
|
330
380
|
activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.name,
|
|
331
381
|
activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.title,
|
|
332
|
-
experimentAssignmentStorage,
|
|
333
|
-
funnelExperiments,
|
|
334
382
|
isPreviewRuntime,
|
|
335
383
|
recordStepCompletion,
|
|
384
|
+
renderSuspended,
|
|
336
385
|
safeActiveStepId,
|
|
337
386
|
]);
|
|
338
387
|
useEffect(() => {
|
|
@@ -363,21 +412,19 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
363
412
|
const nextStepId = useMemo(() => resolveNextStepFromContext({
|
|
364
413
|
stepId: safeActiveStepId,
|
|
365
414
|
attributes,
|
|
366
|
-
userId: user.id,
|
|
367
415
|
safeInitialStepId,
|
|
368
416
|
resolveRenderableStepId,
|
|
369
417
|
getConfiguredNextStepId: (currentStepId, context) => resolveConfiguredNextStep(currentStepId, context),
|
|
370
418
|
getSequentialNextStepId,
|
|
371
|
-
|
|
419
|
+
resolveExperimentVariantKey,
|
|
372
420
|
}), [
|
|
373
421
|
attributes,
|
|
374
|
-
experimentAssignmentStorage,
|
|
375
422
|
getSequentialNextStepId,
|
|
376
423
|
resolveConfiguredNextStep,
|
|
424
|
+
resolveExperimentVariantKey,
|
|
377
425
|
resolveRenderableStepId,
|
|
378
426
|
safeActiveStepId,
|
|
379
427
|
safeInitialStepId,
|
|
380
|
-
user.id,
|
|
381
428
|
]);
|
|
382
429
|
const choiceTargets = getChoiceTargetsForStep(safeActiveStepId);
|
|
383
430
|
const actionBar = (_a = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.actionBar) !== null && _a !== void 0 ? _a : activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.actionBar;
|
|
@@ -387,10 +434,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
387
434
|
const buttonText = (actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonText) || 'Continue';
|
|
388
435
|
const hideActionBar = (actionBar === null || actionBar === void 0 ? void 0 : actionBar.hidden) === true;
|
|
389
436
|
useEffect(() => {
|
|
390
|
-
if (shouldLockToInitialStep) {
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (!autoAdvanceMs) {
|
|
437
|
+
if (!shouldRunAutoAdvanceTimer({ autoAdvanceMs, isPreviewRuntime, shouldLockToInitialStep })) {
|
|
394
438
|
return;
|
|
395
439
|
}
|
|
396
440
|
const autoAdvanceTarget = resolveNextStepId(safeActiveStepId);
|
|
@@ -398,7 +442,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
398
442
|
goToStep(autoAdvanceTarget);
|
|
399
443
|
}, autoAdvanceMs);
|
|
400
444
|
return () => window.clearTimeout(timer);
|
|
401
|
-
}, [autoAdvanceMs, goToStep, resolveNextStepId, safeActiveStepId, shouldLockToInitialStep]);
|
|
445
|
+
}, [autoAdvanceMs, goToStep, isPreviewRuntime, resolveNextStepId, safeActiveStepId, shouldLockToInitialStep]);
|
|
402
446
|
const handleContinue = useCallback(() => {
|
|
403
447
|
goToStep(nextStepId);
|
|
404
448
|
}, [goToStep, nextStepId]);
|
|
@@ -423,53 +467,37 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
423
467
|
setRuntimeMode,
|
|
424
468
|
shouldLockToInitialStep,
|
|
425
469
|
});
|
|
426
|
-
const setEditorVariantSelection = useCallback((experiment,
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
470
|
+
const setEditorVariantSelection = useCallback((experiment, variantKey) => {
|
|
471
|
+
const nextVariantKey = variantKey.trim();
|
|
472
|
+
if (!nextVariantKey) {
|
|
429
473
|
return;
|
|
430
474
|
}
|
|
431
|
-
const selectedVariant = experiment.variants.find((variant) => variant.
|
|
475
|
+
const selectedVariant = experiment.variants.find((variant) => variant.variantKey === nextVariantKey);
|
|
432
476
|
if (!selectedVariant) {
|
|
433
477
|
return;
|
|
434
478
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
try {
|
|
440
|
-
const storageKey = getExperimentAssignmentStorageKey(getRuntimeFunnelIdentity(), userId, experiment.experimentId);
|
|
441
|
-
writeWindowStorageValue(storageKey, selectedVariant.id);
|
|
442
|
-
}
|
|
443
|
-
catch (_a) {
|
|
444
|
-
// ignore storage access failures
|
|
445
|
-
}
|
|
446
|
-
const assignmentKey = getExperimentAssignmentAttributeKey(experiment.experimentId);
|
|
447
|
-
setAttributes((prev) => prev[assignmentKey] === selectedVariant.id
|
|
448
|
-
? prev
|
|
449
|
-
: Object.assign(Object.assign({}, prev), { [assignmentKey]: selectedVariant.id }));
|
|
450
|
-
if (!isPreviewRuntime) {
|
|
451
|
-
const stepMeta = stepById[experiment.stepId];
|
|
452
|
-
apiService.trackFunnelEvent({
|
|
453
|
-
userId,
|
|
454
|
-
eventType: 'experiment_variant_selected_editor',
|
|
455
|
-
stepId: experiment.stepId,
|
|
456
|
-
stepName: (stepMeta === null || stepMeta === void 0 ? void 0 : stepMeta.name) || (stepMeta === null || stepMeta === void 0 ? void 0 : stepMeta.title) || experiment.stepId,
|
|
457
|
-
selected: {
|
|
458
|
-
experimentId: experiment.experimentId,
|
|
459
|
-
variantId: selectedVariant.id,
|
|
460
|
-
variantLabel: selectedVariant.label,
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}, [isPreviewRuntime, stepById, user.id]);
|
|
479
|
+
setEditorVariantOverrides((prev) => (Object.assign(Object.assign({}, prev), { [experiment.experimentId]: selectedVariant.variantKey })));
|
|
480
|
+
}, []);
|
|
465
481
|
const experimentAssignmentForEditor = useCallback((experiment) => {
|
|
466
482
|
var _a;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
483
|
+
const override = editorVariantOverrides[experiment.experimentId];
|
|
484
|
+
if (override) {
|
|
485
|
+
return override;
|
|
486
|
+
}
|
|
487
|
+
if (postHogReady) {
|
|
488
|
+
const assignment = resolveExperimentAssignment({
|
|
489
|
+
experimentId: experiment.experimentId,
|
|
490
|
+
variants: experiment.variants,
|
|
491
|
+
});
|
|
492
|
+
if (assignment) {
|
|
493
|
+
return assignment.variantKey;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return ((_a = experiment.variants[0]) === null || _a === void 0 ? void 0 : _a.variantKey) || '';
|
|
497
|
+
}, [editorVariantOverrides, postHogReady]);
|
|
471
498
|
const contextValue = useMemo(() => ({
|
|
472
499
|
activeStepId: safeActiveStepId,
|
|
500
|
+
isBuilder: isPreviewRuntime,
|
|
473
501
|
goToStep: goToStepFromContext,
|
|
474
502
|
goNext,
|
|
475
503
|
goChoice,
|
|
@@ -494,6 +522,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
494
522
|
goChoice,
|
|
495
523
|
goNext,
|
|
496
524
|
goToStepFromContext,
|
|
525
|
+
isPreviewRuntime,
|
|
497
526
|
resolveRenderableStepId,
|
|
498
527
|
safeActiveStepId,
|
|
499
528
|
setAnswer,
|
|
@@ -512,6 +541,8 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
512
541
|
experimentAssignmentForEditor,
|
|
513
542
|
funnelExperiments,
|
|
514
543
|
handleContinue,
|
|
544
|
+
postHogReady,
|
|
545
|
+
renderSuspended,
|
|
515
546
|
renderedStepId,
|
|
516
547
|
renderedStepMeta,
|
|
517
548
|
runtimeMode,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FunnelStepId } from '../runtime/funnel-runtime';
|
|
1
|
+
import type { FunnelStepId } from '../runtime/funnel-runtime.js';
|
|
2
2
|
export type FunnelBinaryChoice = 'yes' | 'no';
|
|
3
3
|
export type FunnelBirthDate = {
|
|
4
4
|
year: number;
|
|
@@ -14,7 +14,7 @@ export type FunnelTempUpload = {
|
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
16
|
* Typed SDK-facing answers object shared across all steps.
|
|
17
|
-
* Keep this shape in sync with
|
|
17
|
+
* Keep this shape in sync with docs/references/funnel-sdk-user-answers.md so future funnels can extend it safely.
|
|
18
18
|
*/
|
|
19
19
|
export type FunnelUserAnswers = Record<string, unknown> & {
|
|
20
20
|
/** Generic branching answers by step id for yes/no splits. */
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { FunnelUserAnswers } from '../sdk/userAnswers';
|
|
1
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
2
|
+
import { type FunnelUserAttribution } from '../runtime/funnel-attribution.js';
|
|
2
3
|
export type AppUser = {
|
|
3
4
|
id: string;
|
|
4
5
|
name: string;
|
|
5
6
|
email: string;
|
|
6
7
|
attributes: FunnelUserAnswers;
|
|
8
|
+
document: Record<string, unknown>;
|
|
7
9
|
};
|
|
8
10
|
export type FunnelEvent = {
|
|
9
11
|
userId: string;
|
|
@@ -26,7 +28,7 @@ export type StepCompletionEvent = {
|
|
|
26
28
|
export type ManageSubscriptionsResponse = {
|
|
27
29
|
user: {
|
|
28
30
|
id: string;
|
|
29
|
-
|
|
31
|
+
user_id: string;
|
|
30
32
|
email: string | null;
|
|
31
33
|
fullName: string | null;
|
|
32
34
|
subscriptionStatus: string | null;
|
|
@@ -35,8 +37,15 @@ export type ManageSubscriptionsResponse = {
|
|
|
35
37
|
id: string;
|
|
36
38
|
status: string;
|
|
37
39
|
providerSubscriptionId: string;
|
|
40
|
+
providerPlanId: string | null;
|
|
38
41
|
cancelAtPeriodEnd: boolean;
|
|
42
|
+
currentPeriodStart: string | null;
|
|
39
43
|
currentPeriodEnd: string | null;
|
|
44
|
+
createdAt: string | null;
|
|
45
|
+
amountCents?: number | null;
|
|
46
|
+
currency?: string | null;
|
|
47
|
+
billingInterval?: string | null;
|
|
48
|
+
billingIntervalCount?: number | null;
|
|
40
49
|
environment: 'test' | 'live';
|
|
41
50
|
}>;
|
|
42
51
|
eventsCount: number;
|
|
@@ -51,15 +60,23 @@ export type TempPhotoUploadResult = {
|
|
|
51
60
|
declare class ApiService {
|
|
52
61
|
private readFileAsDataUrl;
|
|
53
62
|
getOrCreateClientUserId(): string;
|
|
63
|
+
getSubscriptionManagementUserId(): string | null;
|
|
64
|
+
getSubscriptionManagementStripeCustomerId(): string | null;
|
|
65
|
+
getBootstrapCandidateUserId(fallbackUserId?: string | null): string | null;
|
|
66
|
+
private fetchCurrentUserState;
|
|
67
|
+
private resolveBootstrapUserId;
|
|
54
68
|
bootstrapSession(input?: {
|
|
55
69
|
userId?: string;
|
|
56
70
|
email?: string;
|
|
57
71
|
name?: string;
|
|
72
|
+
attribution?: FunnelUserAttribution;
|
|
73
|
+
}): Promise<AppUser>;
|
|
74
|
+
updateUser(user: AppUser, options?: {
|
|
75
|
+
attribution?: FunnelUserAttribution;
|
|
58
76
|
}): Promise<AppUser>;
|
|
59
|
-
updateUser(user: AppUser): Promise<AppUser>;
|
|
60
77
|
uploadTempPhoto(input: {
|
|
61
78
|
file: File;
|
|
62
|
-
|
|
79
|
+
userId?: string;
|
|
63
80
|
source?: 'camera' | 'upload';
|
|
64
81
|
}): Promise<TempPhotoUploadResult>;
|
|
65
82
|
private sendEventAsync;
|