@funnelsgrove/runtime 0.1.55 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/components/FunnelContext.d.ts +5 -1
  2. package/dist/components/FunnelContext.js +2 -1
  3. package/dist/components/ManageSubscriptionScreen.js +4 -1
  4. package/dist/components/SubscriptionHandoffScreen.d.ts +2 -1
  5. package/dist/components/SubscriptionHandoffScreen.js +11 -6
  6. package/dist/config/funnel.manifest.types.d.ts +4 -2
  7. package/dist/generated/step-contract-hash.d.ts +1 -0
  8. package/dist/generated/step-contract-hash.js +2 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.js +10 -0
  11. package/dist/migrations/step-contract-v2.d.ts +50 -0
  12. package/dist/migrations/step-contract-v2.js +310 -0
  13. package/dist/migrations/step-contract-v3.d.ts +41 -0
  14. package/dist/migrations/step-contract-v3.js +26 -0
  15. package/dist/runtime/funnel-attribution.d.ts +1 -0
  16. package/dist/runtime/funnel-attribution.js +24 -0
  17. package/dist/runtime/funnel-manifest.validation.d.ts +7 -2
  18. package/dist/runtime/funnel-manifest.validation.js +24 -68
  19. package/dist/runtime/funnel-step-lifecycle.d.ts +35 -0
  20. package/dist/runtime/funnel-step-lifecycle.js +49 -0
  21. package/dist/runtime/funnel-step-metadata.validation.d.ts +7 -0
  22. package/dist/runtime/funnel-step-metadata.validation.js +88 -0
  23. package/dist/runtime/submit-email-capture.d.ts +23 -0
  24. package/dist/runtime/submit-email-capture.js +54 -0
  25. package/dist/runtime/use-funnel-flow-controller.d.ts +71 -5
  26. package/dist/runtime/use-funnel-flow-controller.js +850 -116
  27. package/dist/runtime/use-step-choices.d.ts +58 -0
  28. package/dist/runtime/use-step-choices.js +187 -0
  29. package/dist/sdk/userAnswers.d.ts +1 -0
  30. package/dist/services/api.service.d.ts +10 -0
  31. package/dist/services/api.service.js +32 -0
  32. package/dist/services/funnel-sdk.service.d.ts +17 -0
  33. package/dist/services/funnel-sdk.service.js +13 -0
  34. package/dist/steps/choice-contract.d.ts +33 -0
  35. package/dist/steps/choice-contract.js +302 -0
  36. package/dist/steps/step-contract.d.ts +498 -0
  37. package/dist/steps/step-contract.js +506 -0
  38. package/dist/steps/types.d.ts +7 -5
  39. package/dist/steps/types.js +1 -18
  40. package/dist/testing/funnel-contract-journey-driver.d.ts +35 -0
  41. package/dist/testing/funnel-contract-journey-driver.js +266 -0
  42. package/dist/testing/index.d.ts +1 -0
  43. package/dist/testing/index.js +1 -0
  44. package/dist/validation/fixtures/invalid-reserved-identities.d.ts +249 -0
  45. package/dist/validation/fixtures/invalid-reserved-identities.js +20 -0
  46. package/dist/validation/fixtures/valid-v2-project.d.ts +419 -0
  47. package/dist/validation/fixtures/valid-v2-project.js +228 -0
  48. package/dist/validation/fixtures/valid-v3-project.d.ts +419 -0
  49. package/dist/validation/fixtures/valid-v3-project.js +30 -0
  50. package/dist/validation/funnel-contract-diagnostics.d.ts +49 -0
  51. package/dist/validation/funnel-contract-diagnostics.js +67 -0
  52. package/dist/validation/funnel-contract-validator.d.ts +30 -0
  53. package/dist/validation/funnel-contract-validator.js +1458 -0
  54. package/dist/validation/funnel-manifest-input.normalization.d.ts +67 -0
  55. package/dist/validation/funnel-manifest-input.normalization.js +386 -0
  56. package/package.json +9 -1
@@ -15,4 +15,5 @@ export declare const collectCurrentFunnelAttribution: (input?: {
15
15
  posthogProperties?: Record<string, unknown> | null;
16
16
  }) => FunnelUserAttribution;
17
17
  export declare const mergeFunnelUserAttribution: (current: unknown, incoming: FunnelUserAttribution | null | undefined) => FunnelUserAttribution;
18
+ export declare const buildFunnelAttributionEventMetadata: (attribution: FunnelUserAttribution | null | undefined) => Record<string, string>;
18
19
  export declare const getFunnelUserAttribution: (document: unknown) => FunnelUserAttribution | null;
@@ -6,6 +6,22 @@ const INTERNAL_ATTRIBUTION_QUERY_KEYS = new Set([
6
6
  'step',
7
7
  'user_id',
8
8
  ]);
9
+ const EVENT_ATTRIBUTION_CLICK_ID_KEYS = new Set([
10
+ '_fbc',
11
+ '_fbp',
12
+ 'epik',
13
+ 'fbc',
14
+ 'fbclid',
15
+ 'fbp',
16
+ 'gbraid',
17
+ 'gclid',
18
+ 'li_fat_id',
19
+ 'msclkid',
20
+ 'scclid',
21
+ 'ttclid',
22
+ 'twclid',
23
+ 'wbraid',
24
+ ]);
9
25
  const POSTHOG_CONTEXT_MAPPINGS = {
10
26
  $geoip_city_name: 'cityName',
11
27
  $geoip_country_code: 'countryCode',
@@ -41,6 +57,13 @@ const normalizeTouch = (value) => {
41
57
  return [[normalizedKey, normalizedValue]];
42
58
  }));
43
59
  };
60
+ const isEventAttributionKey = (key) => {
61
+ const normalizedKey = key.trim().toLowerCase();
62
+ return normalizedKey.startsWith('utm_') || EVENT_ATTRIBUTION_CLICK_ID_KEYS.has(normalizedKey);
63
+ };
64
+ const normalizeEventAttributionTouch = (value) => {
65
+ return Object.fromEntries(Object.entries(normalizeTouch(value)).filter(([key]) => isEventAttributionKey(key)));
66
+ };
44
67
  const normalizeContext = (value) => {
45
68
  if (!isRecord(value)) {
46
69
  return {};
@@ -219,6 +242,7 @@ export const mergeFunnelUserAttribution = (current, incoming) => {
219
242
  context: deepMergeRecords(currentContext, nextContext),
220
243
  };
221
244
  };
245
+ export const buildFunnelAttributionEventMetadata = (attribution) => normalizeEventAttributionTouch(attribution === null || attribution === void 0 ? void 0 : attribution.firstTouch);
222
246
  export const getFunnelUserAttribution = (document) => {
223
247
  if (!isRecord(document) || !isRecord(document.attribution)) {
224
248
  return null;
@@ -1,2 +1,7 @@
1
- import type { FunnelManifest } from '../config/funnel.manifest.types.js';
2
- export declare const validateFunnelManifest: <T extends FunnelManifest>(manifest: T) => T;
1
+ import { type FunnelManifestContractInput } from '../validation/funnel-contract-validator.js';
2
+ export type LegacyReadableFunnelManifest = FunnelManifestContractInput;
3
+ export declare const normalizeLegacyFunnelManifestForRead: <T extends LegacyReadableFunnelManifest>(manifest: T) => T & {
4
+ stepContractVersion: number;
5
+ };
6
+ export declare const validateFunnelManifest: <T extends LegacyReadableFunnelManifest>(manifest: T) => T;
7
+ export declare const validateAuthoringFunnelManifest: <T extends LegacyReadableFunnelManifest>(manifest: T) => T;
@@ -1,75 +1,31 @@
1
- const isDefinedString = (value) => {
2
- return typeof value === 'string' && value.trim().length > 0;
3
- };
4
- const pushDuplicateErrors = (label, values, errors) => {
5
- const seen = new Set();
6
- const duplicates = new Set();
7
- for (const value of values) {
8
- if (seen.has(value)) {
9
- duplicates.add(value);
10
- continue;
11
- }
12
- seen.add(value);
13
- }
14
- if (duplicates.size > 0) {
15
- errors.push(`duplicate ${label}: ${Array.from(duplicates).join(', ')}`);
1
+ import { LEGACY_UNVERSIONED_STEP_CONTRACT_VERSION, } from '../steps/step-contract.js';
2
+ import { validateFunnelManifestContract, } from '../validation/funnel-contract-validator.js';
3
+ export const normalizeLegacyFunnelManifestForRead = (manifest) => {
4
+ if (manifest.stepContractVersion !== undefined) {
5
+ return manifest;
16
6
  }
7
+ return Object.assign(Object.assign({}, manifest), { stepContractVersion: LEGACY_UNVERSIONED_STEP_CONTRACT_VERSION });
17
8
  };
18
- const assertValidReference = (label, value, validIds, errors) => {
19
- if (!isDefinedString(value) || !validIds.has(value)) {
20
- errors.push(`${label} references missing step "${value || ''}"`);
9
+ const formatDiagnosticValue = (value) => {
10
+ var _a;
11
+ if (typeof value === 'string') {
12
+ return value;
21
13
  }
14
+ return (_a = JSON.stringify(value)) !== null && _a !== void 0 ? _a : String(value);
22
15
  };
23
- export const validateFunnelManifest = (manifest) => {
24
- const errors = [];
25
- if (!Number.isInteger(manifest.templateArchitectureVersion) || manifest.templateArchitectureVersion < 1) {
26
- errors.push('templateArchitectureVersion must be a positive integer');
27
- }
28
- if (manifest.steps.length === 0) {
29
- errors.push('manifest must declare at least one step');
30
- }
31
- const stepIds = manifest.steps.map((step) => step.id);
32
- const stepPaths = manifest.steps.map((step) => step.path);
33
- const filePaths = manifest.steps.map((step) => step.filePath);
34
- const componentKeys = manifest.steps.map((step) => step.componentKey);
35
- const validStepIds = new Set(stepIds);
36
- pushDuplicateErrors('step ids', stepIds, errors);
37
- pushDuplicateErrors('step paths', stepPaths, errors);
38
- pushDuplicateErrors('step file paths', filePaths, errors);
39
- pushDuplicateErrors('step component keys', componentKeys, errors);
40
- for (const entryPoint of manifest.entryPoints || []) {
41
- assertValidReference(`entry point "${entryPoint.id}"`, entryPoint.stepId, validStepIds, errors);
42
- }
43
- for (const [stepId, edges] of Object.entries(manifest.edgesByStepId)) {
44
- assertValidReference(`edge source "${stepId}"`, stepId, validStepIds, errors);
45
- for (const edge of edges || []) {
46
- assertValidReference(`edge from "${stepId}"`, edge.toStepId, validStepIds, errors);
47
- }
48
- }
49
- pushDuplicateErrors('branch ids', (manifest.branches || []).map((branch) => branch.id), errors);
50
- for (const branch of manifest.branches || []) {
51
- if (!isDefinedString(branch.id)) {
52
- errors.push('branch id is required');
53
- }
54
- if (!isDefinedString(branch.name)) {
55
- errors.push(`branch "${branch.id || ''}" name is required`);
56
- }
57
- assertValidReference(`branch "${branch.id}" source`, branch.sourceStepId, validStepIds, errors);
58
- if (branch.stepIds.length === 0) {
59
- errors.push(`branch "${branch.id}" must own at least one step`);
60
- }
61
- for (const stepId of branch.stepIds) {
62
- assertValidReference(`branch "${branch.id}" step`, stepId, validStepIds, errors);
63
- }
64
- }
65
- for (const experiment of manifest.experiments) {
66
- assertValidReference(`experiment "${experiment.experimentId}"`, experiment.stepId, validStepIds, errors);
67
- for (const variant of experiment.variants) {
68
- assertValidReference(`experiment variant "${experiment.experimentId}:${variant.variantKey}"`, variant.routeToStepId, validStepIds, errors);
69
- }
70
- }
71
- if (errors.length > 0) {
72
- throw new Error(`Invalid funnel manifest:\n- ${errors.join('\n- ')}`);
16
+ const validateAuthoringManifest = (manifest) => {
17
+ const diagnostics = validateFunnelManifestContract(manifest, { mode: 'authoring' });
18
+ if (diagnostics.length > 0) {
19
+ const details = diagnostics.map((item) => {
20
+ return `[${item.code}] ${item.reason} Expected ${formatDiagnosticValue(item.expected)}; received ${formatDiagnosticValue(item.received)}.`;
21
+ });
22
+ throw new Error(`Invalid funnel manifest:\n- ${details.join('\n- ')}`);
73
23
  }
74
24
  return manifest;
75
25
  };
26
+ export const validateFunnelManifest = (manifest) => {
27
+ return validateAuthoringManifest(manifest);
28
+ };
29
+ export const validateAuthoringFunnelManifest = (manifest) => {
30
+ return validateAuthoringManifest(manifest);
31
+ };
@@ -0,0 +1,35 @@
1
+ export type FunnelStepExitReason = 'back' | 'replace' | 'cancel' | 'abandon';
2
+ export type FunnelNavigationOutcome = {
3
+ type: 'complete';
4
+ selected?: Record<string, unknown>;
5
+ } | {
6
+ type: 'exit';
7
+ reason: FunnelStepExitReason;
8
+ };
9
+ export type FunnelStepVisit = {
10
+ readonly visitId: number;
11
+ readonly stepId: string;
12
+ readonly startedAt: string;
13
+ readonly outcome: null | 'completed' | 'exited';
14
+ };
15
+ export type FunnelStepLifecycleIntent = {
16
+ readonly type: 'complete';
17
+ readonly selected?: Readonly<Record<string, unknown>>;
18
+ readonly visit: FunnelStepVisit;
19
+ } | {
20
+ readonly type: 'exit';
21
+ readonly reason: FunnelStepExitReason;
22
+ readonly visit: FunnelStepVisit;
23
+ };
24
+ export type FunnelStepLifecycleStartIntent = {
25
+ readonly type: 'start';
26
+ readonly visit: FunnelStepVisit;
27
+ };
28
+ declare const funnelStepLifecycleStateBrand: unique symbol;
29
+ export type FunnelStepLifecycleState = {
30
+ readonly [funnelStepLifecycleStateBrand]: true;
31
+ };
32
+ export declare const createFunnelStepLifecycleState: () => FunnelStepLifecycleState;
33
+ export declare const startVisit: (state: FunnelStepLifecycleState, input: Pick<FunnelStepVisit, "stepId" | "startedAt">) => FunnelStepLifecycleStartIntent | null;
34
+ export declare const finishVisit: (state: FunnelStepLifecycleState, visitId: FunnelStepVisit["visitId"], outcome: FunnelNavigationOutcome) => FunnelStepLifecycleIntent | null;
35
+ export {};
@@ -0,0 +1,49 @@
1
+ const stateValues = new WeakMap();
2
+ const readState = (state) => {
3
+ const value = stateValues.get(state);
4
+ if (!value) {
5
+ throw new Error('Invalid funnel step lifecycle state.');
6
+ }
7
+ return value;
8
+ };
9
+ const snapshotVisit = (visit) => Object.freeze(Object.assign({}, visit));
10
+ export const createFunnelStepLifecycleState = () => {
11
+ const state = Object.freeze({});
12
+ stateValues.set(state, { activeVisit: null, nextVisitId: 1 });
13
+ return state;
14
+ };
15
+ export const startVisit = (state, input) => {
16
+ var _a;
17
+ const stateValue = readState(state);
18
+ if (((_a = stateValue.activeVisit) === null || _a === void 0 ? void 0 : _a.outcome) === null) {
19
+ return null;
20
+ }
21
+ const visit = {
22
+ visitId: stateValue.nextVisitId,
23
+ stepId: input.stepId,
24
+ startedAt: input.startedAt,
25
+ outcome: null,
26
+ };
27
+ stateValue.nextVisitId += 1;
28
+ stateValue.activeVisit = visit;
29
+ return Object.freeze({ type: 'start', visit: snapshotVisit(visit) });
30
+ };
31
+ export const finishVisit = (state, visitId, outcome) => {
32
+ const visit = readState(state).activeVisit;
33
+ if (!visit || visit.visitId !== visitId || visit.outcome !== null) {
34
+ return null;
35
+ }
36
+ visit.outcome = outcome.type === 'complete' ? 'completed' : 'exited';
37
+ const visitSnapshot = snapshotVisit(visit);
38
+ if (outcome.type === 'exit') {
39
+ return Object.freeze({ type: 'exit', reason: outcome.reason, visit: visitSnapshot });
40
+ }
41
+ if (outcome.selected === undefined) {
42
+ return Object.freeze({ type: 'complete', visit: visitSnapshot });
43
+ }
44
+ return Object.freeze({
45
+ type: 'complete',
46
+ selected: Object.freeze(Object.assign({}, outcome.selected)),
47
+ visit: visitSnapshot,
48
+ });
49
+ };
@@ -0,0 +1,7 @@
1
+ import type { FunnelManifestStep } from '../config/funnel.manifest.types.js';
2
+ import type { FunnelStepMeta } from '../steps/types.js';
3
+ import { type FunnelContractDiagnostic } from '../validation/funnel-contract-diagnostics.js';
4
+ export type FunnelStepMetadataParityOptions = {
5
+ file?: string;
6
+ };
7
+ export declare const validateFunnelStepMetadataParity: (manifestStep: FunnelManifestStep, componentMeta: FunnelStepMeta, options?: FunnelStepMetadataParityOptions) => FunnelContractDiagnostic[];
@@ -0,0 +1,88 @@
1
+ import { createFunnelContractDiagnostic, } from '../validation/funnel-contract-diagnostics.js';
2
+ const OMITTED_STEP_KIND = Symbol('omitted-step-kind');
3
+ const normalizeKind = (kind) => {
4
+ return kind === undefined ? OMITTED_STEP_KIND : kind;
5
+ };
6
+ const isChoiceRecord = (value) => {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ };
9
+ const normalizeChoice = (value) => {
10
+ if (!isChoiceRecord(value)) {
11
+ return null;
12
+ }
13
+ const keys = Object.keys(value);
14
+ if (!Object.prototype.hasOwnProperty.call(value, 'answerKey')
15
+ || keys.some((key) => key !== 'answerKey' && key !== 'allowEmpty')
16
+ || typeof value.answerKey !== 'string'
17
+ || (value.allowEmpty !== undefined && typeof value.allowEmpty !== 'boolean')) {
18
+ return null;
19
+ }
20
+ return {
21
+ answerKey: value.answerKey,
22
+ allowEmpty: value.allowEmpty,
23
+ };
24
+ };
25
+ const choicesMatch = (expected, received) => {
26
+ if (expected === undefined || received === undefined) {
27
+ return expected === received;
28
+ }
29
+ const normalizedExpected = normalizeChoice(expected);
30
+ const normalizedReceived = normalizeChoice(received);
31
+ if (normalizedExpected === null || normalizedReceived === null) {
32
+ return false;
33
+ }
34
+ return (normalizedExpected.answerKey === normalizedReceived.answerKey
35
+ && normalizedExpected.allowEmpty === normalizedReceived.allowEmpty);
36
+ };
37
+ export const validateFunnelStepMetadataParity = (manifestStep, componentMeta, options = {}) => {
38
+ var _a;
39
+ const diagnostics = [];
40
+ const comparisons = [
41
+ {
42
+ field: 'id',
43
+ expected: manifestStep.id,
44
+ received: componentMeta.id,
45
+ matches: manifestStep.id === componentMeta.id,
46
+ },
47
+ {
48
+ field: 'name',
49
+ expected: manifestStep.name,
50
+ received: componentMeta.name,
51
+ matches: manifestStep.name === componentMeta.name,
52
+ },
53
+ {
54
+ field: 'type',
55
+ expected: manifestStep.type,
56
+ received: componentMeta.type,
57
+ matches: manifestStep.type === componentMeta.type,
58
+ },
59
+ {
60
+ field: 'kind',
61
+ expected: manifestStep.kind,
62
+ received: componentMeta.kind,
63
+ matches: normalizeKind(manifestStep.kind) === normalizeKind(componentMeta.kind),
64
+ },
65
+ {
66
+ field: 'choice',
67
+ expected: manifestStep.choice,
68
+ received: componentMeta.choice,
69
+ matches: choicesMatch(manifestStep.choice, componentMeta.choice),
70
+ },
71
+ ];
72
+ for (const comparison of comparisons) {
73
+ if (comparison.matches) {
74
+ continue;
75
+ }
76
+ diagnostics.push(createFunnelContractDiagnostic({
77
+ code: 'FG-PARITY-001',
78
+ file: (_a = options.file) !== null && _a !== void 0 ? _a : manifestStep.filePath,
79
+ stepId: manifestStep.id,
80
+ expected: comparison.expected,
81
+ received: comparison.received,
82
+ reason: `Component metadata ${comparison.field} does not match the manifest step.`,
83
+ guide: 'docs/product-specs/funnel-template-step-authoring.md',
84
+ repair: `Update the component metadata ${comparison.field} to match the manifest step.`,
85
+ }));
86
+ }
87
+ return diagnostics;
88
+ };
@@ -0,0 +1,23 @@
1
+ export type EmailCapturePersistenceResult<User> = {
2
+ user: User;
3
+ eventId: string;
4
+ eventCreated: boolean;
5
+ };
6
+ export type EmailCaptureSingleFlight = {
7
+ current: Promise<void> | null;
8
+ };
9
+ export type SubmitEmailCaptureInput<User> = {
10
+ email: string;
11
+ isPreview: boolean;
12
+ singleFlight: EmailCaptureSingleFlight;
13
+ retryPendingNavigation?: () => boolean | Promise<boolean>;
14
+ persist: (email: string) => Promise<EmailCapturePersistenceResult<User>>;
15
+ applyUser: (user: User) => void;
16
+ applyPreviewEmail: (email: string) => void;
17
+ fanOutBrowserDestinations: (eventId: string) => void | Promise<void>;
18
+ completeVisit: () => boolean;
19
+ resolveNextStep: () => string | null;
20
+ navigate: (stepId: string) => void;
21
+ };
22
+ export declare const createEmailCaptureSingleFlight: () => EmailCaptureSingleFlight;
23
+ export declare const submitEmailCapture: <User>(input: SubmitEmailCaptureInput<User>) => Promise<void>;
@@ -0,0 +1,54 @@
1
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2
+ const MAX_EMAIL_LENGTH = 320;
3
+ const normalizeEmail = (value) => {
4
+ const email = value.trim().toLowerCase();
5
+ if (!email || email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email)) {
6
+ throw new Error('Invalid email address');
7
+ }
8
+ return email;
9
+ };
10
+ export const createEmailCaptureSingleFlight = () => ({
11
+ current: null,
12
+ });
13
+ const runEmailCapture = async (input) => {
14
+ if (input.retryPendingNavigation && await input.retryPendingNavigation()) {
15
+ return;
16
+ }
17
+ const email = normalizeEmail(input.email);
18
+ if (input.isPreview) {
19
+ input.applyPreviewEmail(email);
20
+ }
21
+ else {
22
+ const result = await input.persist(email);
23
+ input.applyUser(result.user);
24
+ if (result.eventCreated) {
25
+ try {
26
+ await input.fanOutBrowserDestinations(result.eventId);
27
+ }
28
+ catch (_a) {
29
+ // The server already committed the logical conversion. Browser delivery is best effort.
30
+ }
31
+ }
32
+ }
33
+ if (!input.completeVisit()) {
34
+ return;
35
+ }
36
+ const nextStepId = input.resolveNextStep();
37
+ if (nextStepId) {
38
+ input.navigate(nextStepId);
39
+ }
40
+ };
41
+ export const submitEmailCapture = (input) => {
42
+ if (input.singleFlight.current) {
43
+ return input.singleFlight.current;
44
+ }
45
+ const operation = runEmailCapture(input);
46
+ input.singleFlight.current = operation;
47
+ const clearSingleFlight = () => {
48
+ if (input.singleFlight.current === operation) {
49
+ input.singleFlight.current = null;
50
+ }
51
+ };
52
+ void operation.then(clearSingleFlight, clearSingleFlight);
53
+ return operation;
54
+ };
@@ -2,7 +2,10 @@ import { type Dispatch, type SetStateAction } from 'react';
2
2
  import type { FunnelContextValue } from '../components/FunnelContext.js';
3
3
  import type { FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
4
4
  import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
5
- import type { FunnelStepMeta } from '../steps/types.js';
5
+ import { apiService } from '../services/api.service.js';
6
+ import { type FunnelUserAttribution } from './funnel-attribution.js';
7
+ import type { FunnelStepMeta, FunnelStepType } from '../steps/types.js';
8
+ import { type FunnelStepLifecycleIntent } from './funnel-step-lifecycle.js';
6
9
  export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManifestExperiment & {
7
10
  stepId: StepId;
8
11
  variants: readonly (FunnelManifestExperiment['variants'][number] & {
@@ -11,13 +14,22 @@ export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManife
11
14
  };
12
15
  type StepComponentRegistry = Record<string, unknown>;
13
16
  type FunnelFlowAnalyticsAdapter = {
17
+ flush?: () => unknown | Promise<unknown>;
18
+ trackEmailCapturedBrowserDestinations?: (input: {
19
+ eventId: string;
20
+ stepId: string;
21
+ stepName: string;
22
+ stepType: FunnelStepType;
23
+ }) => void;
14
24
  trackStepStarted?: (input: {
15
25
  userId?: string;
16
26
  environment?: 'test' | 'live';
17
27
  stepId: string;
18
28
  stepName: string;
19
- stepType?: string;
29
+ stepType: FunnelStepType;
20
30
  startedAt: string;
31
+ stepContractVersion: number;
32
+ metadata?: Record<string, unknown>;
21
33
  featureFlags?: Record<string, string>;
22
34
  }) => string | null;
23
35
  trackStepCompleted?: (input: {
@@ -25,10 +37,27 @@ type FunnelFlowAnalyticsAdapter = {
25
37
  environment?: 'test' | 'live';
26
38
  stepId: string;
27
39
  stepName: string;
28
- stepType?: string;
40
+ stepType: FunnelStepType;
29
41
  startedAt: string;
30
42
  endedAt: string;
43
+ stepContractVersion: number;
31
44
  selected?: Record<string, unknown>;
45
+ metadata?: Record<string, unknown>;
46
+ featureFlags?: Record<string, string>;
47
+ }) => string | null;
48
+ trackStepExited?: (input: {
49
+ userId?: string;
50
+ environment?: 'test' | 'live';
51
+ stepId: string;
52
+ stepName: string;
53
+ stepType: FunnelStepType;
54
+ startedAt: string;
55
+ endedAt: string;
56
+ exitReason: Extract<FunnelStepLifecycleIntent, {
57
+ type: 'exit';
58
+ }>['reason'];
59
+ stepContractVersion: number;
60
+ metadata?: Record<string, unknown>;
32
61
  featureFlags?: Record<string, string>;
33
62
  }) => string | null;
34
63
  trackFunnelStarted?: (input: {
@@ -37,7 +66,20 @@ type FunnelFlowAnalyticsAdapter = {
37
66
  stepId: string;
38
67
  stepName: string;
39
68
  stepType?: string;
69
+ stepContractVersion: number;
70
+ occurredAt: string;
71
+ metadata?: Record<string, unknown>;
72
+ featureFlags?: Record<string, string>;
73
+ }) => string | null;
74
+ trackFunnelCompleted?: (input: {
75
+ userId?: string;
76
+ environment?: 'test' | 'live';
77
+ stepId: string;
78
+ stepName: string;
79
+ stepType: FunnelStepType;
80
+ stepContractVersion: number;
40
81
  occurredAt: string;
82
+ metadata?: Record<string, unknown>;
41
83
  featureFlags?: Record<string, string>;
42
84
  }) => string | null;
43
85
  trackFirstStepViewed?: (input: {
@@ -45,13 +87,29 @@ type FunnelFlowAnalyticsAdapter = {
45
87
  environment?: 'test' | 'live';
46
88
  stepId: string;
47
89
  stepName: string;
48
- stepType?: string;
90
+ stepType: FunnelStepType;
91
+ stepContractVersion: number;
92
+ occurredAt: string;
93
+ metadata?: Record<string, unknown>;
94
+ featureFlags?: Record<string, string>;
95
+ }) => string | null;
96
+ trackFirstStepClicked?: (input: {
97
+ userId?: string;
98
+ environment?: 'test' | 'live';
99
+ stepId: string;
100
+ stepName: string;
101
+ stepType: FunnelStepType;
102
+ stepContractVersion: number;
49
103
  occurredAt: string;
104
+ metadata?: Record<string, unknown>;
50
105
  featureFlags?: Record<string, string>;
51
106
  }) => string | null;
52
107
  };
108
+ export type FunnelFlowApiAdapter = Pick<typeof apiService, 'getOrCreateClientUserId' | 'getBootstrapCandidateUserId' | 'bootstrapSession' | 'pingUserContext' | 'updateUser' | 'captureEmail'>;
53
109
  type UseFunnelFlowControllerInput<StepId extends string> = {
110
+ api?: FunnelFlowApiAdapter;
54
111
  analytics?: FunnelFlowAnalyticsAdapter;
112
+ stepContractVersion: number;
55
113
  initialStepId?: StepId;
56
114
  initialAttributes?: FunnelUserAnswers;
57
115
  lockToInitialStep?: boolean;
@@ -99,8 +157,16 @@ type UseFunnelFlowControllerResult<StepId extends string> = {
99
157
  setRuntimeMode: (mode: 'test' | 'live') => void;
100
158
  showShellContinue: boolean;
101
159
  };
160
+ export declare function resolveFunnelEventAttribution(document: unknown, provisionalAttribution: FunnelUserAttribution | null): FunnelUserAttribution | null;
161
+ export declare function buildReadyFunnelAttributionEventMetadata(attributionReady: boolean, attribution: FunnelUserAttribution | null): Record<string, string> | null;
162
+ export declare function scheduleAttributionFailOpen(onReady: () => void, timeoutMs: number): () => void;
163
+ export declare function takeInitialFunnelAttributionMetadata(tracked: boolean, attributionReady: boolean, attribution: FunnelUserAttribution | null): {
164
+ tracked: boolean;
165
+ metadata: Record<string, string> | null;
166
+ };
102
167
  export declare function computeRenderSuspended(input: {
103
168
  activeStepId: string;
169
+ attributionReady?: boolean;
104
170
  postHogReady: boolean;
105
171
  experiments: ReadonlyArray<{
106
172
  stepId: string;
@@ -114,5 +180,5 @@ export declare function resolveRenderedExperimentStepId(input: {
114
180
  resolveRenderableStepId: (stepId: string | null | undefined) => string | null;
115
181
  safeActiveStepId: string;
116
182
  }): string;
117
- export declare function useFunnelFlowController<StepId extends string>({ analytics, initialStepId, initialAttributes, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
183
+ export declare function useFunnelFlowController<StepId extends string>({ api, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
118
184
  export {};