@funnelsgrove/runtime 0.1.0 → 0.1.2

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.
Files changed (62) hide show
  1. package/README.md +20 -1
  2. package/dist/components/FunnelContext.d.ts +5 -2
  3. package/dist/components/FunnelContext.js +3 -0
  4. package/dist/components/FunnelEditorPanel.d.ts +3 -5
  5. package/dist/components/FunnelEditorPanel.js +3 -3
  6. package/dist/components/ManageSubscriptionScreen.d.ts +51 -0
  7. package/dist/components/ManageSubscriptionScreen.js +349 -0
  8. package/dist/components/RuntimeDevInfoBox.d.ts +23 -0
  9. package/dist/components/RuntimeDevInfoBox.js +363 -0
  10. package/dist/components/SubscriptionHandoffScreen.d.ts +31 -0
  11. package/dist/components/SubscriptionHandoffScreen.js +338 -0
  12. package/dist/config/builder-preview.protocol.d.ts +73 -0
  13. package/dist/config/builder-preview.protocol.js +3 -0
  14. package/dist/config/env.config.d.ts +44 -0
  15. package/dist/config/env.config.js +161 -0
  16. package/dist/config/font-config.d.ts +14 -0
  17. package/dist/config/font-config.js +101 -0
  18. package/dist/config/funnel-theme.d.ts +61 -10
  19. package/dist/config/funnel-theme.js +355 -35
  20. package/dist/config/funnel.manifest.types.d.ts +13 -7
  21. package/dist/content/step-content.d.ts +130 -0
  22. package/dist/content/step-content.js +381 -0
  23. package/dist/index.d.ts +33 -21
  24. package/dist/index.js +33 -21
  25. package/dist/runtime/browser-helpers.d.ts +1 -0
  26. package/dist/runtime/browser-helpers.js +14 -0
  27. package/dist/runtime/experiment-assignment.d.ts +13 -4
  28. package/dist/runtime/experiment-assignment.js +9 -27
  29. package/dist/runtime/funnel-attribution.d.ts +18 -0
  30. package/dist/runtime/funnel-attribution.js +226 -0
  31. package/dist/runtime/funnel-flow.d.ts +9 -10
  32. package/dist/runtime/funnel-flow.js +4 -18
  33. package/dist/runtime/funnel-manifest.validation.d.ts +1 -1
  34. package/dist/runtime/funnel-manifest.validation.js +2 -6
  35. package/dist/runtime/funnel-runtime.d.ts +2 -3
  36. package/dist/runtime/funnel-runtime.js +6 -13
  37. package/dist/runtime/posthog-flags.d.ts +30 -0
  38. package/dist/runtime/posthog-flags.js +71 -0
  39. package/dist/runtime/preview-bridge.d.ts +12 -2
  40. package/dist/runtime/preview-bridge.js +95 -4
  41. package/dist/runtime/preview-definition-overrides.d.ts +20 -0
  42. package/dist/runtime/preview-definition-overrides.js +148 -0
  43. package/dist/runtime/route-resolver.d.ts +2 -3
  44. package/dist/runtime/route-resolver.js +15 -26
  45. package/dist/runtime/subscription-handoff.d.ts +32 -0
  46. package/dist/runtime/subscription-handoff.js +113 -0
  47. package/dist/runtime/use-funnel-flow-controller.d.ts +19 -10
  48. package/dist/runtime/use-funnel-flow-controller.js +190 -159
  49. package/dist/sdk/userAnswers.d.ts +2 -2
  50. package/dist/services/api.service.d.ts +21 -4
  51. package/dist/services/api.service.js +165 -35
  52. package/dist/services/funnel-state.service.d.ts +8 -0
  53. package/dist/services/funnel-state.service.js +44 -0
  54. package/dist/services/preview-frame.service.d.ts +2 -2
  55. package/dist/services/preview-frame.service.js +2 -2
  56. package/dist/services/public-env.d.ts +69 -0
  57. package/dist/services/public-env.js +105 -0
  58. package/dist/services/runtime-api.config.d.ts +5 -0
  59. package/dist/services/runtime-api.config.js +12 -7
  60. package/dist/services/runtime-mode.service.d.ts +3 -0
  61. package/dist/services/runtime-mode.service.js +142 -4
  62. package/package.json +8 -2
@@ -1,11 +1,45 @@
1
1
  'use client';
2
2
  import { useEffect, useRef } from 'react';
3
- import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, } from '../config/builder-preview.protocol';
4
- import { isPreviewFrameRuntime } from '../services/preview-frame.service';
5
- import { logger } from '../services/logger';
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';
4
+ import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
5
+ import { logger } from '../services/logger.js';
6
+ import { applyPreviewDefinitionPatch, applyPreviewPaywallPlansPatch } from './preview-definition-overrides.js';
6
7
  const isRecord = (value) => {
7
8
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
8
9
  };
10
+ const parsePreviewDefinitionPatch = (value) => {
11
+ if (!isRecord(value)) {
12
+ return null;
13
+ }
14
+ const kind = typeof value.kind === 'string' ? value.kind : '';
15
+ const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
16
+ if (!kind || !stepId) {
17
+ return null;
18
+ }
19
+ if (kind === 'content') {
20
+ const locale = typeof value.locale === 'string' ? value.locale.trim() : '';
21
+ if (!locale || !isRecord(value.content)) {
22
+ return null;
23
+ }
24
+ return {
25
+ kind,
26
+ stepId,
27
+ locale,
28
+ content: value.content,
29
+ };
30
+ }
31
+ if (kind === 'pricing') {
32
+ if (!isRecord(value.pricing)) {
33
+ return null;
34
+ }
35
+ return {
36
+ kind,
37
+ stepId,
38
+ pricing: value.pricing,
39
+ };
40
+ }
41
+ return null;
42
+ };
9
43
  const isPositiveIndex = (value) => {
10
44
  return typeof value === 'number' && Number.isInteger(value) && value >= 1;
11
45
  };
@@ -85,12 +119,36 @@ export const parsePreviewBridgeMessage = (value) => {
85
119
  const patch = parsePreviewQuickEditPatch(value.patch);
86
120
  return patch ? { kind: 'quickEditPatch', patch } : null;
87
121
  }
122
+ if (messageType === BUILDER_PREVIEW_DEFINITION_PATCH) {
123
+ const patch = parsePreviewDefinitionPatch(value.patch);
124
+ return patch ? { kind: 'definitionPatch', patch } : null;
125
+ }
88
126
  if (messageType === BUILDER_PREVIEW_RUNTIME_MODE_CHANGED) {
89
127
  return {
90
128
  kind: 'runtimeModeChanged',
91
129
  mode: value.mode === 'live' ? 'live' : 'test',
92
130
  };
93
131
  }
132
+ if (messageType === BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED) {
133
+ const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
134
+ const plans = Array.isArray(value.plans)
135
+ ? value.plans.filter((plan) => isRecord(plan) &&
136
+ typeof plan.id === 'string' &&
137
+ typeof plan.title === 'string' &&
138
+ typeof plan.priceLabel === 'string' &&
139
+ typeof plan.perDayAmount === 'string' &&
140
+ typeof plan.perDayLabel === 'string' &&
141
+ typeof plan.amountCents === 'number' &&
142
+ (plan.source === 'stripe' || plan.source === 'config'))
143
+ : [];
144
+ return stepId
145
+ ? {
146
+ kind: 'paywallPlansChanged',
147
+ stepId,
148
+ plans,
149
+ }
150
+ : null;
151
+ }
94
152
  if (messageType === BUILDER_PREVIEW_GO_TO_STEP) {
95
153
  const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
96
154
  return stepId ? { kind: 'goToStep', stepId } : null;
@@ -181,6 +239,25 @@ function emitPreviewReady(stepId) {
181
239
  stepId,
182
240
  }, '*');
183
241
  }
242
+ export function emitPreviewVariableValues(stepId, values) {
243
+ if (typeof window === 'undefined' || window.parent === window) {
244
+ return;
245
+ }
246
+ window.parent.postMessage({
247
+ type: BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED,
248
+ stepId,
249
+ values,
250
+ }, '*');
251
+ }
252
+ export function usePreviewVariableValues(stepId, values) {
253
+ const serializedValues = JSON.stringify(values);
254
+ useEffect(() => {
255
+ if (!isPreviewFrameRuntime()) {
256
+ return;
257
+ }
258
+ emitPreviewVariableValues(stepId, values);
259
+ }, [serializedValues, stepId, values]);
260
+ }
184
261
  export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }) {
185
262
  const previewReadySentRef = useRef(false);
186
263
  useEffect(() => {
@@ -209,6 +286,14 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
209
286
  applyPreviewQuickEditPatch(action.patch);
210
287
  return;
211
288
  }
289
+ if (action.kind === 'definitionPatch') {
290
+ applyPreviewDefinitionPatch(action.patch);
291
+ return;
292
+ }
293
+ if (action.kind === 'paywallPlansChanged') {
294
+ applyPreviewPaywallPlansPatch(action.stepId, action.plans);
295
+ return;
296
+ }
212
297
  if (action.kind === 'runtimeModeChanged') {
213
298
  setRuntimeMode(action.mode);
214
299
  return;
@@ -226,5 +311,11 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
226
311
  return () => {
227
312
  window.removeEventListener('message', handlePreviewMessage);
228
313
  };
229
- }, [activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep]);
314
+ }, [
315
+ activeStepId,
316
+ onGoToStep,
317
+ resolveRenderableStepId,
318
+ setRuntimeMode,
319
+ shouldLockToInitialStep,
320
+ ]);
230
321
  }
@@ -0,0 +1,20 @@
1
+ import { type CountryPricingProfile, type LocalizedStepContent, type ResolvedCountryPricing, type StepEditorSection } from '../content/step-content.js';
2
+ import type { BuilderPreviewDefinitionPatch } from '../config/builder-preview.protocol.js';
3
+ export declare const usePreviewDefinitionOverrideRevision: () => number;
4
+ export declare const clearPreviewDefinitionPatches: () => void;
5
+ export declare const applyPreviewDefinitionPatch: (patch: BuilderPreviewDefinitionPatch) => void;
6
+ export declare const applyPreviewPaywallPlansPatch: (stepId: string, plans: readonly unknown[]) => void;
7
+ export declare const resolvePreviewStepLocalizedContent: <TLocaleContent>(stepId: string, definition: LocalizedStepContent<TLocaleContent>, requestedLocale?: string | null, options?: {
8
+ editorSections?: readonly StepEditorSection[];
9
+ variableValues?: Record<string, string>;
10
+ universalVariables?: Record<string, string>;
11
+ }) => TLocaleContent;
12
+ export declare const resolvePreviewStepCountryPricing: <TOfferKey extends string>(stepId: string, pricingProfile: CountryPricingProfile<TOfferKey>, requestedCountryCode?: string | null) => ResolvedCountryPricing<TOfferKey>;
13
+ export declare const usePreviewStepLocalizedContent: <TLocaleContent>(stepId: string, definition: LocalizedStepContent<TLocaleContent>, requestedLocale?: string | null, options?: {
14
+ editorSections?: readonly StepEditorSection[];
15
+ variableValues?: Record<string, string>;
16
+ universalVariables?: Record<string, string>;
17
+ }) => TLocaleContent;
18
+ export declare const usePreviewStepCountryPricing: <TOfferKey extends string>(stepId: string, pricingProfile: CountryPricingProfile<TOfferKey>, requestedCountryCode?: string | null) => ResolvedCountryPricing<TOfferKey>;
19
+ export declare const resolvePreviewStepPaywallPlans: <TPlan>(stepId: string) => readonly TPlan[] | null;
20
+ export declare const usePreviewStepPaywallPlans: <TPlan>(stepId: string) => readonly TPlan[] | null;
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+ import { useMemo, useSyncExternalStore } from 'react';
3
+ import { resolveStepContentVariables, resolveUniversalContentVariables, resolveCountryPricing, resolveLocalizedStepContent, } from '../content/step-content.js';
4
+ import { useOptionalFunnel } from '../components/FunnelContext.js';
5
+ import { apiService } from '../services/api.service.js';
6
+ const PREVIEW_DEFINITION_OVERRIDE_STORE_KEY = '__funnelsgrovePreviewDefinitionOverrides';
7
+ const getPreviewDefinitionOverrideStore = () => {
8
+ const globalState = globalThis;
9
+ const existingStore = globalState[PREVIEW_DEFINITION_OVERRIDE_STORE_KEY];
10
+ if (existingStore) {
11
+ return existingStore;
12
+ }
13
+ const nextStore = {
14
+ listeners: new Set(),
15
+ state: {
16
+ revision: 0,
17
+ contentByStepId: {},
18
+ pricingByStepId: {},
19
+ paywallPlansByStepId: {},
20
+ },
21
+ };
22
+ globalState[PREVIEW_DEFINITION_OVERRIDE_STORE_KEY] = nextStore;
23
+ return nextStore;
24
+ };
25
+ const normalizeStepId = (value) => {
26
+ return value.trim();
27
+ };
28
+ const normalizeLocale = (value) => {
29
+ return value.trim().toLowerCase();
30
+ };
31
+ const subscribePreviewDefinitionOverrides = (listener) => {
32
+ const store = getPreviewDefinitionOverrideStore();
33
+ store.listeners.add(listener);
34
+ return () => {
35
+ store.listeners.delete(listener);
36
+ };
37
+ };
38
+ const emitPreviewDefinitionOverrideChange = () => {
39
+ for (const listener of getPreviewDefinitionOverrideStore().listeners) {
40
+ listener();
41
+ }
42
+ };
43
+ const getPreviewDefinitionOverrideRevision = () => {
44
+ return getPreviewDefinitionOverrideStore().state.revision;
45
+ };
46
+ export const usePreviewDefinitionOverrideRevision = () => {
47
+ return useSyncExternalStore(subscribePreviewDefinitionOverrides, getPreviewDefinitionOverrideRevision, () => 0);
48
+ };
49
+ const buildMergedLocalizedContentDefinition = (stepId, definition) => {
50
+ const stepOverrides = getPreviewDefinitionOverrideStore().state.contentByStepId[normalizeStepId(stepId)];
51
+ if (!stepOverrides) {
52
+ return definition;
53
+ }
54
+ const mergedLocales = Object.assign({}, definition.locales);
55
+ for (const [locale, localeOverride] of Object.entries(stepOverrides)) {
56
+ const currentLocaleValue = definition.locales[locale];
57
+ if (currentLocaleValue && typeof currentLocaleValue === 'object' && localeOverride) {
58
+ mergedLocales[locale] = Object.assign(Object.assign({}, currentLocaleValue), localeOverride);
59
+ continue;
60
+ }
61
+ mergedLocales[locale] = localeOverride;
62
+ }
63
+ return Object.assign(Object.assign({}, definition), { locales: mergedLocales });
64
+ };
65
+ export const clearPreviewDefinitionPatches = () => {
66
+ const store = getPreviewDefinitionOverrideStore();
67
+ store.state = {
68
+ revision: store.state.revision + 1,
69
+ contentByStepId: {},
70
+ pricingByStepId: {},
71
+ paywallPlansByStepId: {},
72
+ };
73
+ emitPreviewDefinitionOverrideChange();
74
+ };
75
+ export const applyPreviewDefinitionPatch = (patch) => {
76
+ const store = getPreviewDefinitionOverrideStore();
77
+ const stepId = normalizeStepId(patch.stepId);
78
+ if (!stepId) {
79
+ return;
80
+ }
81
+ if (patch.kind === 'content') {
82
+ const locale = normalizeLocale(patch.locale);
83
+ if (!locale) {
84
+ return;
85
+ }
86
+ store.state = Object.assign(Object.assign({}, store.state), { revision: store.state.revision + 1, contentByStepId: Object.assign(Object.assign({}, store.state.contentByStepId), { [stepId]: Object.assign(Object.assign({}, (store.state.contentByStepId[stepId] || {})), { [locale]: patch.content }) }) });
87
+ emitPreviewDefinitionOverrideChange();
88
+ return;
89
+ }
90
+ store.state = Object.assign(Object.assign({}, store.state), { revision: store.state.revision + 1, pricingByStepId: Object.assign(Object.assign({}, store.state.pricingByStepId), { [stepId]: patch.pricing }) });
91
+ emitPreviewDefinitionOverrideChange();
92
+ };
93
+ export const applyPreviewPaywallPlansPatch = (stepId, plans) => {
94
+ const normalizedStepId = normalizeStepId(stepId);
95
+ if (!normalizedStepId) {
96
+ return;
97
+ }
98
+ const store = getPreviewDefinitionOverrideStore();
99
+ store.state = Object.assign(Object.assign({}, store.state), { revision: store.state.revision + 1, paywallPlansByStepId: Object.assign(Object.assign({}, store.state.paywallPlansByStepId), { [normalizedStepId]: [...plans] }) });
100
+ emitPreviewDefinitionOverrideChange();
101
+ };
102
+ export const resolvePreviewStepLocalizedContent = (stepId, definition, requestedLocale, options) => {
103
+ var _a;
104
+ const localizedContent = resolveLocalizedStepContent(buildMergedLocalizedContentDefinition(stepId, definition), requestedLocale);
105
+ if (!((_a = options === null || options === void 0 ? void 0 : options.editorSections) === null || _a === void 0 ? void 0 : _a.length) || !options.variableValues) {
106
+ if (!(options === null || options === void 0 ? void 0 : options.universalVariables)) {
107
+ return localizedContent;
108
+ }
109
+ return resolveUniversalContentVariables(localizedContent, options.universalVariables);
110
+ }
111
+ const contentWithEditorVariables = resolveStepContentVariables(localizedContent, options.editorSections, options.variableValues);
112
+ if (!options.universalVariables) {
113
+ return contentWithEditorVariables;
114
+ }
115
+ return resolveUniversalContentVariables(contentWithEditorVariables, options.universalVariables);
116
+ };
117
+ export const resolvePreviewStepCountryPricing = (stepId, pricingProfile, requestedCountryCode) => {
118
+ const pricingOverride = getPreviewDefinitionOverrideStore().state.pricingByStepId[normalizeStepId(stepId)];
119
+ return resolveCountryPricing(pricingOverride || pricingProfile, requestedCountryCode);
120
+ };
121
+ export const usePreviewStepLocalizedContent = (stepId, definition, requestedLocale, options) => {
122
+ const revision = usePreviewDefinitionOverrideRevision();
123
+ const funnel = useOptionalFunnel();
124
+ const currentUserId = (funnel === null || funnel === void 0 ? void 0 : funnel.user.id) || apiService.getOrCreateClientUserId();
125
+ return useMemo(() => {
126
+ var _a;
127
+ void revision;
128
+ return resolvePreviewStepLocalizedContent(stepId, definition, requestedLocale, Object.assign(Object.assign({}, options), { universalVariables: Object.assign(Object.assign({}, ((_a = options === null || options === void 0 ? void 0 : options.universalVariables) !== null && _a !== void 0 ? _a : {})), { user_id: currentUserId }) }));
129
+ }, [currentUserId, definition, options, requestedLocale, revision, stepId]);
130
+ };
131
+ export const usePreviewStepCountryPricing = (stepId, pricingProfile, requestedCountryCode) => {
132
+ const revision = usePreviewDefinitionOverrideRevision();
133
+ return useMemo(() => {
134
+ void revision;
135
+ return resolvePreviewStepCountryPricing(stepId, pricingProfile, requestedCountryCode);
136
+ }, [pricingProfile, requestedCountryCode, revision, stepId]);
137
+ };
138
+ export const resolvePreviewStepPaywallPlans = (stepId) => {
139
+ const plans = getPreviewDefinitionOverrideStore().state.paywallPlansByStepId[normalizeStepId(stepId)];
140
+ return Array.isArray(plans) ? plans : null;
141
+ };
142
+ export const usePreviewStepPaywallPlans = (stepId) => {
143
+ const revision = usePreviewDefinitionOverrideRevision();
144
+ return useMemo(() => {
145
+ void revision;
146
+ return resolvePreviewStepPaywallPlans(stepId);
147
+ }, [revision, stepId]);
148
+ };
@@ -1,4 +1,4 @@
1
- import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types';
1
+ import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
2
2
  export declare const getChoiceTargetsFromEdges: (manifest: FunnelManifest, stepId: string) => {
3
3
  yes?: string;
4
4
  no?: string;
@@ -8,11 +8,10 @@ export declare const getPathForStep: (manifest: FunnelManifest, stepId: string)
8
8
  export declare const resolveInitialStepId: (input: {
9
9
  manifest: FunnelManifest;
10
10
  requestedStepId?: string | null;
11
- entryPointId?: string | null;
12
11
  }) => string;
13
12
  export declare const resolveNextStepId: (input: {
14
13
  manifest: FunnelManifest;
15
14
  currentStepId: string;
16
15
  attributes: Record<string, unknown>;
17
- getAssignedVariantId: (experiment: FunnelManifestExperiment) => string | null;
16
+ resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
18
17
  }) => string | null;
@@ -4,21 +4,12 @@ const getStepById = (manifest, stepId) => {
4
4
  }
5
5
  return manifest.steps.find((step) => step.id === stepId) || null;
6
6
  };
7
- const getEntryPointStepId = (manifest, entryPointId) => {
7
+ const getDefaultManifestStepId = (manifest) => {
8
8
  var _a;
9
- if (!entryPointId) {
10
- return null;
11
- }
12
- return ((_a = manifest.entryPoints.find((entryPoint) => entryPoint.id === entryPointId)) === null || _a === void 0 ? void 0 : _a.stepId) || null;
9
+ return ((_a = manifest.steps[0]) === null || _a === void 0 ? void 0 : _a.id) || '';
13
10
  };
14
- const getDefaultEntryPointStepId = (manifest) => {
15
- var _a, _b;
16
- return (((_a = manifest.entryPoints.find((entryPoint) => 'isDefault' in entryPoint && entryPoint.isDefault)) === null || _a === void 0 ? void 0 : _a.stepId) ||
17
- ((_b = manifest.steps[0]) === null || _b === void 0 ? void 0 : _b.id) ||
18
- '');
19
- };
20
- const getActiveExperimentForStep = (manifest, stepId) => {
21
- return (manifest.experiments.find((experiment) => experiment.stepId === stepId && experiment.status === 'active') || null);
11
+ const getExperimentForStep = (manifest, stepId) => {
12
+ return manifest.experiments.find((experiment) => experiment.stepId === stepId) || null;
22
13
  };
23
14
  const resolveNextStepFromEdges = (edges, attributes) => {
24
15
  var _a;
@@ -69,23 +60,21 @@ export const resolveInitialStepId = (input) => {
69
60
  if (requestedStep) {
70
61
  return requestedStep.id;
71
62
  }
72
- const entryPointStepId = getEntryPointStepId(input.manifest, input.entryPointId);
73
- const entryPointStep = getStepById(input.manifest, entryPointStepId);
74
- if (entryPointStep) {
75
- return entryPointStep.id;
76
- }
77
- return getDefaultEntryPointStepId(input.manifest);
63
+ return getDefaultManifestStepId(input.manifest);
78
64
  };
79
65
  export const resolveNextStepId = (input) => {
80
66
  var _a;
81
- const activeExperiment = getActiveExperimentForStep(input.manifest, input.currentStepId);
82
- if (activeExperiment) {
83
- const assignedVariantId = input.getAssignedVariantId(activeExperiment);
84
- const assignedVariant = activeExperiment.variants.find((variant) => variant.id === assignedVariantId);
85
- if (assignedVariant) {
86
- return assignedVariant.routeToStepId;
67
+ const experiment = getExperimentForStep(input.manifest, input.currentStepId);
68
+ if (experiment) {
69
+ const variantKey = input.resolveExperimentVariantKey(experiment);
70
+ const matchedVariant = variantKey
71
+ ? experiment.variants.find((variant) => variant.variantKey === variantKey)
72
+ : undefined;
73
+ if (matchedVariant) {
74
+ return matchedVariant.routeToStepId;
87
75
  }
88
- return ((_a = activeExperiment.variants[0]) === null || _a === void 0 ? void 0 : _a.routeToStepId) || null;
76
+ const controlVariant = (_a = experiment.variants.find((variant) => variant.variantKey === 'control')) !== null && _a !== void 0 ? _a : experiment.variants[0];
77
+ return (controlVariant === null || controlVariant === void 0 ? void 0 : controlVariant.routeToStepId) || null;
89
78
  }
90
79
  return resolveNextStepFromEdges(input.manifest.edgesByStepId[input.currentStepId], input.attributes);
91
80
  };
@@ -0,0 +1,32 @@
1
+ import type { FunnelUserAttribution } from './funnel-attribution.js';
2
+ export type SubscriptionHandoffPlatform = 'ios' | 'android' | 'desktop';
3
+ export type SubscriptionHandoffConfig = {
4
+ iosAppStoreUrl: string | null;
5
+ androidPlayStoreUrl: string | null;
6
+ universalLink: string | null;
7
+ iosDeepLink: string | null;
8
+ androidDeepLink: string | null;
9
+ };
10
+ export type ResolvedSubscriptionHandoff = {
11
+ platform: SubscriptionHandoffPlatform;
12
+ iosStoreUrl: string | null;
13
+ androidStoreUrl: string | null;
14
+ iosDeepLinkUrl: string | null;
15
+ androidDeepLinkUrl: string | null;
16
+ openAppUrl: string | null;
17
+ webLinkUrl: string | null;
18
+ emailOpenAppUrl: string | null;
19
+ emailIosDeepLinkUrl: string | null;
20
+ emailAndroidDeepLinkUrl: string | null;
21
+ emailIosStoreUrl: string | null;
22
+ emailAndroidStoreUrl: string | null;
23
+ qrConfirmationUrl: string | null;
24
+ };
25
+ export declare const resolveSubscriptionHandoff: (input: {
26
+ userId: string | null | undefined;
27
+ email?: string | null;
28
+ confirmationUrl: string | null | undefined;
29
+ userAgent?: string | null;
30
+ attribution?: FunnelUserAttribution | null;
31
+ config: SubscriptionHandoffConfig;
32
+ }) => ResolvedSubscriptionHandoff;
@@ -0,0 +1,113 @@
1
+ const asTrimmedString = (value) => {
2
+ if (typeof value !== 'string') {
3
+ return null;
4
+ }
5
+ const trimmed = value.trim();
6
+ return trimmed || null;
7
+ };
8
+ const resolvePlatform = (userAgent) => {
9
+ var _a;
10
+ const normalized = ((_a = asTrimmedString(userAgent)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '';
11
+ if (/iphone|ipad|ipod/.test(normalized)) {
12
+ return 'ios';
13
+ }
14
+ if (/android/.test(normalized)) {
15
+ return 'android';
16
+ }
17
+ return 'desktop';
18
+ };
19
+ const applyLinkTemplates = (value, variables) => {
20
+ return value
21
+ .replace(/\{user_id\}/g, encodeURIComponent(variables.userId))
22
+ .replace(/\{email\}/g, variables.email ? encodeURIComponent(variables.email) : '');
23
+ };
24
+ const resolveUrl = (value, variables, firstTouch) => {
25
+ const normalized = asTrimmedString(value);
26
+ if (!normalized) {
27
+ return null;
28
+ }
29
+ try {
30
+ const url = new URL(applyLinkTemplates(normalized, variables));
31
+ if (!url.searchParams.has('user_id')) {
32
+ url.searchParams.set('user_id', variables.userId);
33
+ }
34
+ for (const [key, rawValue] of Object.entries(firstTouch)) {
35
+ const nextKey = key.trim();
36
+ const nextValue = rawValue.trim();
37
+ if (!nextKey || !nextValue || url.searchParams.has(nextKey)) {
38
+ continue;
39
+ }
40
+ url.searchParams.set(nextKey, nextValue);
41
+ }
42
+ return url.toString();
43
+ }
44
+ catch (_a) {
45
+ return applyLinkTemplates(normalized, variables);
46
+ }
47
+ };
48
+ const buildQrConfirmationUrl = (confirmationUrl, userId) => {
49
+ const normalized = asTrimmedString(confirmationUrl);
50
+ if (!normalized) {
51
+ return null;
52
+ }
53
+ try {
54
+ const url = new URL(normalized);
55
+ url.search = '';
56
+ url.hash = '';
57
+ url.searchParams.set('user_id', userId);
58
+ return url.toString();
59
+ }
60
+ catch (_a) {
61
+ return null;
62
+ }
63
+ };
64
+ export const resolveSubscriptionHandoff = (input) => {
65
+ var _a, _b;
66
+ const userId = asTrimmedString(input.userId);
67
+ const email = asTrimmedString(input.email);
68
+ const platform = resolvePlatform(input.userAgent);
69
+ const firstTouch = (_b = (_a = input.attribution) === null || _a === void 0 ? void 0 : _a.firstTouch) !== null && _b !== void 0 ? _b : {};
70
+ if (!userId) {
71
+ return {
72
+ platform,
73
+ iosStoreUrl: null,
74
+ androidStoreUrl: null,
75
+ iosDeepLinkUrl: null,
76
+ androidDeepLinkUrl: null,
77
+ openAppUrl: null,
78
+ webLinkUrl: null,
79
+ emailOpenAppUrl: null,
80
+ emailIosDeepLinkUrl: null,
81
+ emailAndroidDeepLinkUrl: null,
82
+ emailIosStoreUrl: null,
83
+ emailAndroidStoreUrl: null,
84
+ qrConfirmationUrl: null,
85
+ };
86
+ }
87
+ const variables = { userId, email };
88
+ const iosStoreUrl = resolveUrl(input.config.iosAppStoreUrl, variables, firstTouch);
89
+ const androidStoreUrl = resolveUrl(input.config.androidPlayStoreUrl, variables, firstTouch);
90
+ const iosDeepLink = resolveUrl(input.config.iosDeepLink, variables, firstTouch);
91
+ const androidDeepLink = resolveUrl(input.config.androidDeepLink, variables, firstTouch);
92
+ const universalLink = resolveUrl(input.config.universalLink, variables, firstTouch);
93
+ const openAppUrl = platform === 'ios'
94
+ ? iosDeepLink || universalLink || iosStoreUrl
95
+ : platform === 'android'
96
+ ? androidDeepLink || universalLink || androidStoreUrl
97
+ : null;
98
+ return {
99
+ platform,
100
+ iosStoreUrl,
101
+ androidStoreUrl,
102
+ iosDeepLinkUrl: iosDeepLink,
103
+ androidDeepLinkUrl: androidDeepLink,
104
+ openAppUrl,
105
+ webLinkUrl: universalLink,
106
+ emailOpenAppUrl: universalLink || iosDeepLink || androidDeepLink,
107
+ emailIosDeepLinkUrl: iosDeepLink,
108
+ emailAndroidDeepLinkUrl: androidDeepLink,
109
+ emailIosStoreUrl: iosStoreUrl,
110
+ emailAndroidStoreUrl: androidStoreUrl,
111
+ qrConfirmationUrl: buildQrConfirmationUrl(input.confirmationUrl, userId),
112
+ };
113
+ };
@@ -1,20 +1,21 @@
1
- import { type Dispatch, type SetStateAction, type ComponentType } from 'react';
2
- import type { FunnelContextValue } from '../components/FunnelContext';
3
- import type { FunnelManifestExperiment } from '../config/funnel.manifest.types';
4
- import type { FunnelStepMeta } from '../steps/types';
1
+ import { type Dispatch, type SetStateAction } from 'react';
2
+ import type { FunnelContextValue } from '../components/FunnelContext.js';
3
+ import type { FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
4
+ import type { FunnelStepMeta } from '../steps/types.js';
5
5
  export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManifestExperiment & {
6
6
  stepId: StepId;
7
7
  variants: readonly (FunnelManifestExperiment['variants'][number] & {
8
8
  routeToStepId: StepId;
9
9
  })[];
10
10
  };
11
+ type StepComponentRegistry = Record<string, unknown>;
11
12
  type UseFunnelFlowControllerInput<StepId extends string> = {
12
13
  initialStepId?: StepId;
13
14
  lockToInitialStep?: boolean;
14
15
  defaultStepId: StepId;
15
16
  stepSequence: readonly StepId[];
16
17
  stepById: Record<string, FunnelStepMeta>;
17
- stepComponentById: Record<string, ComponentType>;
18
+ stepComponentById: StepComponentRegistry;
18
19
  funnelExperiments: readonly FunnelFlowControllerExperiment<StepId>[];
19
20
  getPathForStep: (stepId: StepId) => string;
20
21
  getStepIdFromPath: (pathname: string) => StepId | null;
@@ -26,16 +27,14 @@ type UseFunnelFlowControllerInput<StepId extends string> = {
26
27
  isFunnelStepId: (value: string) => value is StepId;
27
28
  resolveConfiguredNextStep: (stepId: StepId, context: {
28
29
  attributes: Record<string, unknown>;
29
- userId: string;
30
- getAssignedVariantId: (experiment: FunnelManifestExperiment) => string | null;
30
+ resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
31
31
  }) => StepId | null;
32
32
  resolveRuntimeInitialStepId: (input: {
33
33
  requestedStepId?: string | null;
34
- entryPointId?: string | null;
35
34
  }) => StepId;
36
35
  };
37
36
  type UseFunnelFlowControllerResult<StepId extends string> = {
38
- activeStepComponent: ComponentType | undefined;
37
+ activeStepComponent: unknown;
39
38
  activeStepId: StepId;
40
39
  activeStepMeta: FunnelStepMeta | undefined;
41
40
  buttonText: string;
@@ -46,13 +45,23 @@ type UseFunnelFlowControllerResult<StepId extends string> = {
46
45
  experimentAssignmentForEditor: (experiment: FunnelFlowControllerExperiment<StepId>) => string;
47
46
  funnelExperiments: readonly FunnelFlowControllerExperiment<StepId>[];
48
47
  handleContinue: () => void;
48
+ postHogReady: boolean;
49
+ renderSuspended: boolean;
49
50
  renderedStepId: StepId;
50
51
  renderedStepMeta: FunnelStepMeta | undefined;
51
52
  runtimeMode: 'test' | 'live';
52
53
  setEditorPanelExpanded: Dispatch<SetStateAction<boolean>>;
53
- setEditorVariantSelection: (experiment: FunnelFlowControllerExperiment<StepId>, variantId: string) => void;
54
+ setEditorVariantSelection: (experiment: FunnelFlowControllerExperiment<StepId>, variantKey: string) => void;
54
55
  setRuntimeMode: (mode: 'test' | 'live') => void;
55
56
  showShellContinue: boolean;
56
57
  };
58
+ export declare function computeRenderSuspended(input: {
59
+ activeStepId: string;
60
+ postHogReady: boolean;
61
+ experiments: ReadonlyArray<{
62
+ stepId: string;
63
+ }>;
64
+ isPreviewRuntime?: boolean;
65
+ }): boolean;
57
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>;
58
67
  export {};