@funnelsgrove/runtime 0.1.56 → 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 (54) 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-manifest.validation.d.ts +7 -2
  16. package/dist/runtime/funnel-manifest.validation.js +24 -68
  17. package/dist/runtime/funnel-step-lifecycle.d.ts +35 -0
  18. package/dist/runtime/funnel-step-lifecycle.js +49 -0
  19. package/dist/runtime/funnel-step-metadata.validation.d.ts +7 -0
  20. package/dist/runtime/funnel-step-metadata.validation.js +88 -0
  21. package/dist/runtime/submit-email-capture.d.ts +23 -0
  22. package/dist/runtime/submit-email-capture.js +54 -0
  23. package/dist/runtime/use-funnel-flow-controller.d.ts +58 -5
  24. package/dist/runtime/use-funnel-flow-controller.js +804 -122
  25. package/dist/runtime/use-step-choices.d.ts +58 -0
  26. package/dist/runtime/use-step-choices.js +187 -0
  27. package/dist/sdk/userAnswers.d.ts +1 -0
  28. package/dist/services/api.service.d.ts +10 -0
  29. package/dist/services/api.service.js +32 -0
  30. package/dist/services/funnel-sdk.service.d.ts +17 -0
  31. package/dist/services/funnel-sdk.service.js +13 -0
  32. package/dist/steps/choice-contract.d.ts +33 -0
  33. package/dist/steps/choice-contract.js +302 -0
  34. package/dist/steps/step-contract.d.ts +498 -0
  35. package/dist/steps/step-contract.js +506 -0
  36. package/dist/steps/types.d.ts +7 -5
  37. package/dist/steps/types.js +1 -18
  38. package/dist/testing/funnel-contract-journey-driver.d.ts +35 -0
  39. package/dist/testing/funnel-contract-journey-driver.js +266 -0
  40. package/dist/testing/index.d.ts +1 -0
  41. package/dist/testing/index.js +1 -0
  42. package/dist/validation/fixtures/invalid-reserved-identities.d.ts +249 -0
  43. package/dist/validation/fixtures/invalid-reserved-identities.js +20 -0
  44. package/dist/validation/fixtures/valid-v2-project.d.ts +419 -0
  45. package/dist/validation/fixtures/valid-v2-project.js +228 -0
  46. package/dist/validation/fixtures/valid-v3-project.d.ts +419 -0
  47. package/dist/validation/fixtures/valid-v3-project.js +30 -0
  48. package/dist/validation/funnel-contract-diagnostics.d.ts +49 -0
  49. package/dist/validation/funnel-contract-diagnostics.js +67 -0
  50. package/dist/validation/funnel-contract-validator.d.ts +30 -0
  51. package/dist/validation/funnel-contract-validator.js +1458 -0
  52. package/dist/validation/funnel-manifest-input.normalization.d.ts +67 -0
  53. package/dist/validation/funnel-manifest-input.normalization.js +386 -0
  54. package/package.json +9 -1
@@ -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,8 +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 { apiService } from '../services/api.service.js';
5
6
  import { type FunnelUserAttribution } from './funnel-attribution.js';
6
- import type { FunnelStepMeta } from '../steps/types.js';
7
+ import type { FunnelStepMeta, FunnelStepType } from '../steps/types.js';
8
+ import { type FunnelStepLifecycleIntent } from './funnel-step-lifecycle.js';
7
9
  export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManifestExperiment & {
8
10
  stepId: StepId;
9
11
  variants: readonly (FunnelManifestExperiment['variants'][number] & {
@@ -12,13 +14,21 @@ export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManife
12
14
  };
13
15
  type StepComponentRegistry = Record<string, unknown>;
14
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;
15
24
  trackStepStarted?: (input: {
16
25
  userId?: string;
17
26
  environment?: 'test' | 'live';
18
27
  stepId: string;
19
28
  stepName: string;
20
- stepType?: string;
29
+ stepType: FunnelStepType;
21
30
  startedAt: string;
31
+ stepContractVersion: number;
22
32
  metadata?: Record<string, unknown>;
23
33
  featureFlags?: Record<string, string>;
24
34
  }) => string | null;
@@ -27,19 +37,47 @@ type FunnelFlowAnalyticsAdapter = {
27
37
  environment?: 'test' | 'live';
28
38
  stepId: string;
29
39
  stepName: string;
30
- stepType?: string;
40
+ stepType: FunnelStepType;
31
41
  startedAt: string;
32
42
  endedAt: string;
43
+ stepContractVersion: number;
33
44
  selected?: Record<string, unknown>;
34
45
  metadata?: Record<string, unknown>;
35
46
  featureFlags?: Record<string, string>;
36
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>;
61
+ featureFlags?: Record<string, string>;
62
+ }) => string | null;
37
63
  trackFunnelStarted?: (input: {
38
64
  userId?: string;
39
65
  environment?: 'test' | 'live';
40
66
  stepId: string;
41
67
  stepName: string;
42
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;
43
81
  occurredAt: string;
44
82
  metadata?: Record<string, unknown>;
45
83
  featureFlags?: Record<string, string>;
@@ -49,14 +87,29 @@ type FunnelFlowAnalyticsAdapter = {
49
87
  environment?: 'test' | 'live';
50
88
  stepId: string;
51
89
  stepName: string;
52
- 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;
53
103
  occurredAt: string;
54
104
  metadata?: Record<string, unknown>;
55
105
  featureFlags?: Record<string, string>;
56
106
  }) => string | null;
57
107
  };
108
+ export type FunnelFlowApiAdapter = Pick<typeof apiService, 'getOrCreateClientUserId' | 'getBootstrapCandidateUserId' | 'bootstrapSession' | 'pingUserContext' | 'updateUser' | 'captureEmail'>;
58
109
  type UseFunnelFlowControllerInput<StepId extends string> = {
110
+ api?: FunnelFlowApiAdapter;
59
111
  analytics?: FunnelFlowAnalyticsAdapter;
112
+ stepContractVersion: number;
60
113
  initialStepId?: StepId;
61
114
  initialAttributes?: FunnelUserAnswers;
62
115
  lockToInitialStep?: boolean;
@@ -127,5 +180,5 @@ export declare function resolveRenderedExperimentStepId(input: {
127
180
  resolveRenderableStepId: (stepId: string | null | undefined) => string | null;
128
181
  safeActiveStepId: string;
129
182
  }): string;
130
- 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>;
131
184
  export {};