@funnelsgrove/runtime 0.1.18 → 0.1.20
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 +1 -0
- package/dist/components/SubscriptionHandoffScreen.js +51 -18
- package/dist/config/builder-preview.protocol.d.ts +5 -0
- package/dist/config/builder-preview.protocol.js +1 -0
- package/dist/config/funnel.experiments.types.d.ts +6 -3
- package/dist/config/funnel.experiments.types.js +3 -1
- package/dist/runtime/posthog-flags.d.ts +1 -0
- package/dist/runtime/posthog-flags.js +6 -0
- package/dist/runtime/preview-bridge.d.ts +4 -0
- package/dist/runtime/preview-bridge.js +33 -1
- package/dist/runtime/use-funnel-flow-controller.d.ts +10 -1
- package/dist/runtime/use-funnel-flow-controller.js +19 -1
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@ export type FunnelContextValue = {
|
|
|
33
33
|
setAttribute: (key: string, value: unknown) => void;
|
|
34
34
|
attributes: FunnelUserAnswers;
|
|
35
35
|
user: FunnelUser;
|
|
36
|
+
userBootstrapped: boolean;
|
|
36
37
|
setUser: (user: FunnelUser) => void;
|
|
37
38
|
/** Manually record a step as completed (used by custom step components). */
|
|
38
39
|
completeStep: (stepId: string, choices?: Record<string, unknown>) => void;
|
|
@@ -44,7 +44,7 @@ const renderStoreIcon = (variant) => {
|
|
|
44
44
|
return _jsx(WebLinkIcon, {});
|
|
45
45
|
};
|
|
46
46
|
export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, nodeId, }) {
|
|
47
|
-
const { user } = useFunnel();
|
|
47
|
+
const { isBuilder, user, userBootstrapped } = useFunnel();
|
|
48
48
|
const [autoOpenAttempted, setAutoOpenAttempted] = useState(false);
|
|
49
49
|
const [emailSent, setEmailSent] = useState(false);
|
|
50
50
|
const browserSnapshotKey = useSyncExternalStore(subscribeToBrowserSnapshot, () => [
|
|
@@ -61,10 +61,11 @@ export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, no
|
|
|
61
61
|
userAgent: userAgent || null,
|
|
62
62
|
};
|
|
63
63
|
}, [browserSnapshotKey]);
|
|
64
|
+
const handoffReady = isBuilder || (userBootstrapped && user.email.trim().length > 0);
|
|
64
65
|
const handoff = useMemo(() => {
|
|
65
66
|
return resolveSubscriptionHandoff({
|
|
66
|
-
userId: user.id,
|
|
67
|
-
email: user.email,
|
|
67
|
+
userId: handoffReady ? user.id : null,
|
|
68
|
+
email: handoffReady ? user.email : null,
|
|
68
69
|
confirmationUrl: browserSnapshot.confirmationUrl,
|
|
69
70
|
userAgent: browserSnapshot.userAgent,
|
|
70
71
|
attribution: getFunnelUserAttribution(user.document),
|
|
@@ -76,50 +77,60 @@ export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, no
|
|
|
76
77
|
androidDeepLink: runtimePublicConfig.androidDeepLink,
|
|
77
78
|
},
|
|
78
79
|
});
|
|
79
|
-
}, [browserSnapshot, user.document, user.email, user.id]);
|
|
80
|
+
}, [browserSnapshot, handoffReady, user.document, user.email, user.id]);
|
|
80
81
|
const qrCodeImageUrl = useMemo(() => buildQrCodeImageUrl(handoff.qrConfirmationUrl), [handoff.qrConfirmationUrl]);
|
|
81
82
|
const installLinks = useMemo(() => {
|
|
82
83
|
const links = [];
|
|
83
84
|
const iosLinkContent = getStoreLinkContent(content.storeLinks, 'ios');
|
|
84
|
-
if (handoff.iosStoreUrl && iosLinkContent) {
|
|
85
|
+
if ((handoff.iosStoreUrl || (!handoffReady && runtimePublicConfig.iosAppStoreUrl)) && iosLinkContent) {
|
|
85
86
|
links.push({
|
|
86
87
|
id: 'ios',
|
|
87
|
-
href: handoff.iosStoreUrl,
|
|
88
|
+
href: handoffReady ? handoff.iosStoreUrl : null,
|
|
88
89
|
eyebrow: iosLinkContent.eyebrow,
|
|
89
90
|
title: iosLinkContent.title,
|
|
90
91
|
variant: 'ios',
|
|
92
|
+
disabled: !handoffReady,
|
|
91
93
|
});
|
|
92
94
|
}
|
|
93
95
|
const androidLinkContent = getStoreLinkContent(content.storeLinks, 'android');
|
|
94
|
-
if (handoff.androidStoreUrl &&
|
|
96
|
+
if ((handoff.androidStoreUrl || (!handoffReady && runtimePublicConfig.androidPlayStoreUrl)) &&
|
|
97
|
+
androidLinkContent) {
|
|
95
98
|
links.push({
|
|
96
99
|
id: 'android',
|
|
97
|
-
href: handoff.androidStoreUrl,
|
|
100
|
+
href: handoffReady ? handoff.androidStoreUrl : null,
|
|
98
101
|
eyebrow: androidLinkContent.eyebrow,
|
|
99
102
|
title: androidLinkContent.title,
|
|
100
103
|
variant: 'android',
|
|
104
|
+
disabled: !handoffReady,
|
|
101
105
|
});
|
|
102
106
|
}
|
|
103
107
|
const webLinkContent = getStoreLinkContent(content.storeLinks, 'web');
|
|
104
108
|
const shouldShowWebLink = handoff.platform === 'desktop' || handoff.platform === 'android';
|
|
105
|
-
if (shouldShowWebLink &&
|
|
109
|
+
if (shouldShowWebLink &&
|
|
110
|
+
(handoff.webLinkUrl || (!handoffReady && runtimePublicConfig.universalLink)) &&
|
|
111
|
+
webLinkContent) {
|
|
106
112
|
links.push({
|
|
107
113
|
id: 'web',
|
|
108
|
-
href: handoff.webLinkUrl,
|
|
114
|
+
href: handoffReady ? handoff.webLinkUrl : null,
|
|
109
115
|
eyebrow: webLinkContent.eyebrow,
|
|
110
116
|
title: webLinkContent.title,
|
|
111
117
|
variant: 'web',
|
|
118
|
+
disabled: !handoffReady,
|
|
112
119
|
});
|
|
113
120
|
}
|
|
114
121
|
return links;
|
|
115
122
|
}, [
|
|
116
123
|
content.storeLinks,
|
|
117
124
|
handoff.androidStoreUrl,
|
|
125
|
+
handoffReady,
|
|
118
126
|
handoff.iosStoreUrl,
|
|
119
127
|
handoff.platform,
|
|
120
128
|
handoff.webLinkUrl,
|
|
121
129
|
]);
|
|
122
130
|
const emailHref = useMemo(() => {
|
|
131
|
+
if (!handoffReady) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
123
134
|
const lines = [
|
|
124
135
|
content.emailIntro,
|
|
125
136
|
'',
|
|
@@ -142,10 +153,19 @@ export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, no
|
|
|
142
153
|
subject: content.emailSubject,
|
|
143
154
|
lines,
|
|
144
155
|
});
|
|
145
|
-
}, [content, handoff, user.email]);
|
|
156
|
+
}, [content, handoff, handoffReady, user.email]);
|
|
157
|
+
const hasConfiguredOpenAppTarget = handoff.platform === 'ios'
|
|
158
|
+
? Boolean(runtimePublicConfig.iosDeepLink || runtimePublicConfig.universalLink || runtimePublicConfig.iosAppStoreUrl)
|
|
159
|
+
: handoff.platform === 'android'
|
|
160
|
+
? Boolean(runtimePublicConfig.androidDeepLink ||
|
|
161
|
+
runtimePublicConfig.universalLink ||
|
|
162
|
+
runtimePublicConfig.androidPlayStoreUrl)
|
|
163
|
+
: false;
|
|
164
|
+
const shouldShowOpenAppButton = handoff.platform !== 'desktop' &&
|
|
165
|
+
(Boolean(handoff.openAppUrl) || (!handoffReady && hasConfiguredOpenAppTarget));
|
|
146
166
|
useEffect(() => {
|
|
147
167
|
const openAppUrl = handoff.openAppUrl;
|
|
148
|
-
if (handoff.platform === 'desktop' || typeof window === 'undefined' || !openAppUrl) {
|
|
168
|
+
if (!handoffReady || handoff.platform === 'desktop' || typeof window === 'undefined' || !openAppUrl) {
|
|
149
169
|
return;
|
|
150
170
|
}
|
|
151
171
|
const attemptKey = `app-deals:deep-link-attempt:${stepId}:${user.id}`;
|
|
@@ -153,13 +173,18 @@ export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, no
|
|
|
153
173
|
return;
|
|
154
174
|
}
|
|
155
175
|
window.sessionStorage.setItem(attemptKey, '1');
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
window.location.assign(openAppUrl);
|
|
177
|
+
const statusTimer = window.setTimeout(() => {
|
|
158
178
|
setAutoOpenAttempted(true);
|
|
159
|
-
},
|
|
160
|
-
return () => window.clearTimeout(
|
|
161
|
-
}, [handoff.openAppUrl, handoff.platform, stepId, user.id]);
|
|
162
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: 'subscription-handoff-step', "data-node-id": nodeId || stepId, children: [_jsxs("header", { className: 'subscription-handoff-head', children: [_jsx("p", { className: 'subscription-handoff-kicker', children: content.kicker }), _jsx("h2", { className: 'subscription-handoff-title', children: content.title })] }), installLinks.length > 0 ? (_jsx("div", { className: 'subscription-handoff-store-links', children: installLinks.map((link) =>
|
|
179
|
+
}, 0);
|
|
180
|
+
return () => window.clearTimeout(statusTimer);
|
|
181
|
+
}, [handoff.openAppUrl, handoff.platform, handoffReady, stepId, user.id]);
|
|
182
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: 'subscription-handoff-step', "data-node-id": nodeId || stepId, children: [_jsxs("header", { className: 'subscription-handoff-head', children: [_jsx("p", { className: 'subscription-handoff-kicker', children: content.kicker }), _jsx("h2", { className: 'subscription-handoff-title', children: content.title })] }), installLinks.length > 0 ? (_jsx("div", { className: 'subscription-handoff-store-links', children: installLinks.map((link) => {
|
|
183
|
+
const label = `${link.eyebrow} ${link.title}`;
|
|
184
|
+
const className = `subscription-handoff-store-badge is-${link.variant}${link.disabled ? ' is-disabled' : ''}`;
|
|
185
|
+
const body = (_jsxs(_Fragment, { children: [renderStoreIcon(link.variant), _jsxs("span", { className: 'subscription-handoff-store-copy', children: [_jsx("span", { className: 'subscription-handoff-store-eyebrow', children: link.eyebrow }), _jsx("span", { className: 'subscription-handoff-store-title', children: link.title })] })] }));
|
|
186
|
+
return link.href ? (_jsx("a", { href: link.href, target: '_blank', rel: 'noopener noreferrer', "aria-label": label, className: className, children: body }, link.id)) : (_jsx("span", { role: 'link', "aria-label": label, "aria-disabled": 'true', "aria-busy": 'true', className: className, children: body }, link.id));
|
|
187
|
+
}) })) : null, _jsx("p", { className: 'subscription-handoff-copy', children: qrCodeImageUrl ? content.copyWithQr : content.copyWithoutQr }), qrCodeImageUrl && handoff.qrConfirmationUrl ? (_jsx("a", { href: handoff.qrConfirmationUrl, className: 'subscription-handoff-qr-card', target: '_blank', rel: 'noopener noreferrer', "aria-label": content.qrOpenLabel, children: _jsx("span", { "aria-hidden": 'true', className: 'subscription-handoff-qr-image', style: { backgroundImage: `url(${qrCodeImageUrl})` } }) })) : null, _jsx("p", { className: 'subscription-handoff-note', children: content.note }), shouldShowOpenAppButton && handoff.openAppUrl ? (_jsx("a", { href: handoff.openAppUrl, className: 'subscription-handoff-open-app', children: content.openAppLabel })) : null, shouldShowOpenAppButton && !handoff.openAppUrl ? (_jsx("span", { role: 'link', "aria-disabled": 'true', "aria-busy": 'true', className: 'subscription-handoff-open-app is-disabled', children: content.openAppLabel })) : null, emailHref ? (_jsx("a", { href: emailHref, className: 'subscription-handoff-email-button', onClick: () => setEmailSent(true), children: content.emailButtonLabel })) : (_jsx("span", { role: 'link', "aria-disabled": 'true', "aria-busy": 'true', className: 'subscription-handoff-email-button is-disabled', children: content.emailButtonLabel })), emailSent ? (_jsx("p", { className: 'subscription-handoff-status', children: content.emailSentStatus })) : null, autoOpenAttempted ? (_jsx("p", { className: 'subscription-handoff-status', children: content.autoOpenStatus })) : null] }), _jsx("style", { children: subscriptionHandoffScreenStyles })] }));
|
|
163
188
|
}
|
|
164
189
|
const subscriptionHandoffScreenStyles = `
|
|
165
190
|
.subscription-handoff-step {
|
|
@@ -229,6 +254,14 @@ const subscriptionHandoffScreenStyles = `
|
|
|
229
254
|
color: var(--color-text, #262729);
|
|
230
255
|
}
|
|
231
256
|
|
|
257
|
+
.subscription-handoff-store-badge.is-disabled,
|
|
258
|
+
.subscription-handoff-open-app.is-disabled,
|
|
259
|
+
.subscription-handoff-email-button.is-disabled {
|
|
260
|
+
opacity: 0.55;
|
|
261
|
+
cursor: progress;
|
|
262
|
+
pointer-events: none;
|
|
263
|
+
}
|
|
264
|
+
|
|
232
265
|
.subscription-handoff-store-icon {
|
|
233
266
|
width: 28px;
|
|
234
267
|
height: 28px;
|
|
@@ -5,6 +5,7 @@ export declare const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = "builder.preview.run
|
|
|
5
5
|
export declare const BUILDER_PREVIEW_DEFINITION_PATCH = "builder.preview.definitionPatch";
|
|
6
6
|
export declare const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = "builder.preview.variableValuesChanged";
|
|
7
7
|
export declare const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = "builder.preview.paywallPlansChanged";
|
|
8
|
+
export declare const BUILDER_PREVIEW_THEME_CHANGED = "builder.preview.themeChanged";
|
|
8
9
|
export type BuilderPreviewReadyMessage = {
|
|
9
10
|
type: typeof BUILDER_PREVIEW_READY;
|
|
10
11
|
stepId: string;
|
|
@@ -75,3 +76,7 @@ export type BuilderPreviewPaywallPlansChangedMessage = {
|
|
|
75
76
|
stepId: string;
|
|
76
77
|
plans: BuilderPreviewPaywallPlan[];
|
|
77
78
|
};
|
|
79
|
+
export type BuilderPreviewThemeChangedMessage = {
|
|
80
|
+
type: typeof BUILDER_PREVIEW_THEME_CHANGED;
|
|
81
|
+
cssVariables: Record<string, string>;
|
|
82
|
+
};
|
|
@@ -5,3 +5,4 @@ export const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = 'builder.preview.runtimeMode
|
|
|
5
5
|
export const BUILDER_PREVIEW_DEFINITION_PATCH = 'builder.preview.definitionPatch';
|
|
6
6
|
export const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = 'builder.preview.variableValuesChanged';
|
|
7
7
|
export const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = 'builder.preview.paywallPlansChanged';
|
|
8
|
+
export const BUILDER_PREVIEW_THEME_CHANGED = 'builder.preview.themeChanged';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { FunnelManifestExperiment } from './funnel.manifest.types.js';
|
|
2
|
-
export type
|
|
2
|
+
export type FunnelExperimentDefinition = {
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
5
|
-
type: 'paywall';
|
|
5
|
+
type: 'paywall' | 'step';
|
|
6
|
+
status?: 'running' | 'paused' | 'stopped';
|
|
6
7
|
launchDate: string;
|
|
7
8
|
control: {
|
|
8
9
|
stepId: string;
|
|
@@ -13,6 +14,8 @@ export type PaywallExperimentDefinition = {
|
|
|
13
14
|
trafficPercent: number;
|
|
14
15
|
};
|
|
15
16
|
};
|
|
16
|
-
export type
|
|
17
|
+
export type PaywallExperimentDefinition = FunnelExperimentDefinition & {
|
|
18
|
+
type: 'paywall';
|
|
19
|
+
};
|
|
17
20
|
export declare const defineFunnelExperiments: <const T extends readonly FunnelExperimentDefinition[]>(experiments: T) => T;
|
|
18
21
|
export declare const toManifestExperiments: (experiments: readonly FunnelExperimentDefinition[]) => FunnelManifestExperiment[];
|
|
@@ -7,7 +7,9 @@ export const defineFunnelExperiments = (experiments) => {
|
|
|
7
7
|
}
|
|
8
8
|
return experiments;
|
|
9
9
|
};
|
|
10
|
-
export const toManifestExperiments = (experiments) => experiments
|
|
10
|
+
export const toManifestExperiments = (experiments) => experiments
|
|
11
|
+
.filter((experiment) => { var _a; return ((_a = experiment.status) !== null && _a !== void 0 ? _a : 'running') === 'running'; })
|
|
12
|
+
.map((experiment) => ({
|
|
11
13
|
experimentId: experiment.id,
|
|
12
14
|
stepId: experiment.control.stepId,
|
|
13
15
|
variants: [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import posthog from 'posthog-js';
|
|
2
2
|
let initialized = false;
|
|
3
3
|
let readyPromise = null;
|
|
4
|
+
export const POSTHOG_FEATURE_FLAG_READY_TIMEOUT_MS = 2500;
|
|
4
5
|
export const bootstrapPostHog = (config) => {
|
|
5
6
|
if (readyPromise) {
|
|
6
7
|
return readyPromise;
|
|
@@ -10,13 +11,18 @@ export const bootstrapPostHog = (config) => {
|
|
|
10
11
|
resolve();
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
14
|
+
let readyTimeoutId = null;
|
|
13
15
|
const markReady = () => {
|
|
14
16
|
if (initialized) {
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
19
|
initialized = true;
|
|
20
|
+
if (readyTimeoutId) {
|
|
21
|
+
clearTimeout(readyTimeoutId);
|
|
22
|
+
}
|
|
18
23
|
resolve();
|
|
19
24
|
};
|
|
25
|
+
readyTimeoutId = setTimeout(markReady, POSTHOG_FEATURE_FLAG_READY_TIMEOUT_MS);
|
|
20
26
|
posthog.init(config.apiKey, {
|
|
21
27
|
api_host: config.apiHost,
|
|
22
28
|
persistence: 'localStorage+cookie',
|
|
@@ -41,12 +41,16 @@ type PreviewBridgeMessage = {
|
|
|
41
41
|
kind: 'paywallPlansChanged';
|
|
42
42
|
stepId: string;
|
|
43
43
|
plans: BuilderPreviewPaywallPlan[];
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'themeChanged';
|
|
46
|
+
cssVariables: Record<string, string>;
|
|
44
47
|
} | {
|
|
45
48
|
kind: 'goToStep';
|
|
46
49
|
stepId: string;
|
|
47
50
|
};
|
|
48
51
|
export declare const parsePreviewQuickEditPatch: (value: unknown) => PreviewQuickEditPatch | null;
|
|
49
52
|
export declare const parsePreviewBridgeMessage: (value: unknown) => PreviewBridgeMessage | null;
|
|
53
|
+
export declare const applyPreviewThemeVariables: (cssVariables: Record<string, string>) => void;
|
|
50
54
|
export declare const applyPreviewQuickEditPatch: (patch: PreviewQuickEditPatch) => void;
|
|
51
55
|
export declare function emitPreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
|
|
52
56
|
export declare function usePreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_DEFINITION_PATCH, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED, } from '../config/builder-preview.protocol.js';
|
|
3
|
+
import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_DEFINITION_PATCH, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, BUILDER_PREVIEW_THEME_CHANGED, BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED, } from '../config/builder-preview.protocol.js';
|
|
4
4
|
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
5
5
|
import { logger } from '../services/logger.js';
|
|
6
6
|
import { applyPreviewDefinitionPatch, applyPreviewPaywallPlansPatch } from './preview-definition-overrides.js';
|
|
@@ -107,6 +107,13 @@ export const parsePreviewQuickEditPatch = (value) => {
|
|
|
107
107
|
}
|
|
108
108
|
return null;
|
|
109
109
|
};
|
|
110
|
+
const parseThemeCssVariables = (value) => {
|
|
111
|
+
if (!isRecord(value)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const cssVariables = Object.fromEntries(Object.entries(value).filter((entry) => entry[0].startsWith('--') && typeof entry[1] === 'string'));
|
|
115
|
+
return Object.keys(cssVariables).length > 0 ? cssVariables : null;
|
|
116
|
+
};
|
|
110
117
|
export const parsePreviewBridgeMessage = (value) => {
|
|
111
118
|
if (!isRecord(value)) {
|
|
112
119
|
return null;
|
|
@@ -149,12 +156,33 @@ export const parsePreviewBridgeMessage = (value) => {
|
|
|
149
156
|
}
|
|
150
157
|
: null;
|
|
151
158
|
}
|
|
159
|
+
if (messageType === BUILDER_PREVIEW_THEME_CHANGED) {
|
|
160
|
+
const cssVariables = parseThemeCssVariables(value.cssVariables);
|
|
161
|
+
return cssVariables ? { kind: 'themeChanged', cssVariables } : null;
|
|
162
|
+
}
|
|
152
163
|
if (messageType === BUILDER_PREVIEW_GO_TO_STEP) {
|
|
153
164
|
const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
|
|
154
165
|
return stepId ? { kind: 'goToStep', stepId } : null;
|
|
155
166
|
}
|
|
156
167
|
return null;
|
|
157
168
|
};
|
|
169
|
+
export const applyPreviewThemeVariables = (cssVariables) => {
|
|
170
|
+
if (typeof document === 'undefined') {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const targets = [
|
|
174
|
+
document.documentElement,
|
|
175
|
+
document.body,
|
|
176
|
+
...Array.from(document.querySelectorAll('.page-root')),
|
|
177
|
+
].filter((target) => Boolean(target));
|
|
178
|
+
for (const target of targets) {
|
|
179
|
+
for (const [key, value] of Object.entries(cssVariables)) {
|
|
180
|
+
if (key.startsWith('--')) {
|
|
181
|
+
target.style.setProperty(key, value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
158
186
|
export const applyPreviewQuickEditPatch = (patch) => {
|
|
159
187
|
if (typeof document === 'undefined') {
|
|
160
188
|
return;
|
|
@@ -291,6 +319,10 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
|
|
|
291
319
|
applyPreviewPaywallPlansPatch(action.stepId, action.plans);
|
|
292
320
|
return;
|
|
293
321
|
}
|
|
322
|
+
if (action.kind === 'themeChanged') {
|
|
323
|
+
applyPreviewThemeVariables(action.cssVariables);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
294
326
|
if (action.kind === 'runtimeModeChanged') {
|
|
295
327
|
setRuntimeMode(action.mode);
|
|
296
328
|
return;
|
|
@@ -9,7 +9,16 @@ export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManife
|
|
|
9
9
|
})[];
|
|
10
10
|
};
|
|
11
11
|
type StepComponentRegistry = Record<string, unknown>;
|
|
12
|
+
type FunnelFlowAnalyticsAdapter = {
|
|
13
|
+
trackFirstStepViewed?: (input: {
|
|
14
|
+
stepId: string;
|
|
15
|
+
stepName: string;
|
|
16
|
+
occurredAt: string;
|
|
17
|
+
featureFlags?: Record<string, string>;
|
|
18
|
+
}) => string | null;
|
|
19
|
+
};
|
|
12
20
|
type UseFunnelFlowControllerInput<StepId extends string> = {
|
|
21
|
+
analytics?: FunnelFlowAnalyticsAdapter;
|
|
13
22
|
initialStepId?: StepId;
|
|
14
23
|
lockToInitialStep?: boolean;
|
|
15
24
|
defaultStepId: StepId;
|
|
@@ -63,5 +72,5 @@ export declare function computeRenderSuspended(input: {
|
|
|
63
72
|
}>;
|
|
64
73
|
isPreviewRuntime?: boolean;
|
|
65
74
|
}): boolean;
|
|
66
|
-
export declare function useFunnelFlowController<StepId extends string>({ initialStepId, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
|
|
75
|
+
export declare function useFunnelFlowController<StepId extends string>({ analytics, initialStepId, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
|
|
67
76
|
export {};
|
|
@@ -54,7 +54,7 @@ export function computeRenderSuspended(input) {
|
|
|
54
54
|
}
|
|
55
55
|
return input.experiments.some((experiment) => experiment.stepId === input.activeStepId);
|
|
56
56
|
}
|
|
57
|
-
export function useFunnelFlowController({ initialStepId, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
|
|
57
|
+
export function useFunnelFlowController({ analytics, initialStepId, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
|
|
58
58
|
var _a, _b;
|
|
59
59
|
const [runtimeMode, setRuntimeMode] = useRuntimeMode();
|
|
60
60
|
const [editorModeEnabled, setEditorModeEnabled] = useState(false);
|
|
@@ -102,10 +102,12 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
102
102
|
document: {},
|
|
103
103
|
completedSteps: [],
|
|
104
104
|
}));
|
|
105
|
+
const [userBootstrapped, setUserBootstrapped] = useState(isPreviewRuntime);
|
|
105
106
|
const attributesAtStepStart = useRef({});
|
|
106
107
|
const attributesRef = useRef(attributes);
|
|
107
108
|
const postHogFeatureFlagsRef = useRef({});
|
|
108
109
|
const prevStepIdRef = useRef(null);
|
|
110
|
+
const firstStepViewedTrackedRef = useRef(false);
|
|
109
111
|
const stepStartedAtByIdRef = useRef({});
|
|
110
112
|
const engagedStepIdsRef = useRef(new Set());
|
|
111
113
|
const currentUserIdRef = useRef(user.id);
|
|
@@ -245,6 +247,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
245
247
|
const urlUserAttributes = resolveUrlUserAttributes();
|
|
246
248
|
const bootstrapCandidateUserId = apiService.getBootstrapCandidateUserId(localUserId);
|
|
247
249
|
const initialAttribution = collectCurrentFunnelAttribution();
|
|
250
|
+
setUserBootstrapped(isPreviewRuntime);
|
|
248
251
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId })));
|
|
249
252
|
if (isPreviewRuntime) {
|
|
250
253
|
return;
|
|
@@ -274,6 +277,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
274
277
|
setAttributes((prev) => (Object.assign(Object.assign({}, sessionAttributes), prev)));
|
|
275
278
|
}
|
|
276
279
|
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 })));
|
|
280
|
+
setUserBootstrapped(true);
|
|
277
281
|
void apiService.pingUserContext(sessionUser.id)
|
|
278
282
|
.then((pingedUser) => {
|
|
279
283
|
if (!pingedUser) {
|
|
@@ -292,6 +296,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
292
296
|
})
|
|
293
297
|
.catch((error) => {
|
|
294
298
|
logger.error('Failed to bootstrap user session:', error);
|
|
299
|
+
setUserBootstrapped(false);
|
|
295
300
|
return null;
|
|
296
301
|
});
|
|
297
302
|
Promise.all([postHogBootstrap, sessionBootstrap])
|
|
@@ -368,6 +373,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
368
373
|
recordStepCompletion(stepId, choices);
|
|
369
374
|
}, [recordStepCompletion]);
|
|
370
375
|
useEffect(() => {
|
|
376
|
+
var _a;
|
|
371
377
|
if (safeActiveStepId !== activeStepId) {
|
|
372
378
|
logger.warn(`[FunnelFlow] Unknown or non-renderable step "${activeStepId}", falling back to "${safeActiveStepId}".`);
|
|
373
379
|
setActiveStepId(safeActiveStepId);
|
|
@@ -391,6 +397,15 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
391
397
|
startedAt,
|
|
392
398
|
});
|
|
393
399
|
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, environment: runtimeMode, step_id: renderedStepId, step_name: stepName, started_at: startedAt }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
400
|
+
if (!firstStepViewedTrackedRef.current && safeActiveStepId === defaultStepId) {
|
|
401
|
+
firstStepViewedTrackedRef.current = true;
|
|
402
|
+
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackFirstStepViewed) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
403
|
+
stepId: renderedStepId,
|
|
404
|
+
stepName,
|
|
405
|
+
occurredAt: startedAt,
|
|
406
|
+
featureFlags: postHogFeatureFlagsRef.current,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
394
409
|
}
|
|
395
410
|
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
396
411
|
prevStepIdRef.current = renderedStepId;
|
|
@@ -425,6 +440,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
425
440
|
return () => window.clearTimeout(timer);
|
|
426
441
|
}, [
|
|
427
442
|
activeStepId,
|
|
443
|
+
analytics,
|
|
428
444
|
defaultStepId,
|
|
429
445
|
isPreviewRuntime,
|
|
430
446
|
recordStepCompletion,
|
|
@@ -563,6 +579,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
563
579
|
setAttribute,
|
|
564
580
|
attributes,
|
|
565
581
|
user,
|
|
582
|
+
userBootstrapped,
|
|
566
583
|
setUser,
|
|
567
584
|
completeStep,
|
|
568
585
|
}), [
|
|
@@ -580,6 +597,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
580
597
|
setAnswer,
|
|
581
598
|
setAttribute,
|
|
582
599
|
user,
|
|
600
|
+
userBootstrapped,
|
|
583
601
|
]);
|
|
584
602
|
return {
|
|
585
603
|
activeStepComponent,
|