@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.
- package/dist/components/FunnelContext.d.ts +5 -1
- package/dist/components/FunnelContext.js +2 -1
- package/dist/components/ManageSubscriptionScreen.js +4 -1
- package/dist/components/SubscriptionHandoffScreen.d.ts +2 -1
- package/dist/components/SubscriptionHandoffScreen.js +11 -6
- package/dist/config/funnel.manifest.types.d.ts +4 -2
- package/dist/generated/step-contract-hash.d.ts +1 -0
- package/dist/generated/step-contract-hash.js +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/migrations/step-contract-v2.d.ts +50 -0
- package/dist/migrations/step-contract-v2.js +310 -0
- package/dist/migrations/step-contract-v3.d.ts +41 -0
- package/dist/migrations/step-contract-v3.js +26 -0
- package/dist/runtime/funnel-manifest.validation.d.ts +7 -2
- package/dist/runtime/funnel-manifest.validation.js +24 -68
- package/dist/runtime/funnel-step-lifecycle.d.ts +35 -0
- package/dist/runtime/funnel-step-lifecycle.js +49 -0
- package/dist/runtime/funnel-step-metadata.validation.d.ts +7 -0
- package/dist/runtime/funnel-step-metadata.validation.js +88 -0
- package/dist/runtime/submit-email-capture.d.ts +23 -0
- package/dist/runtime/submit-email-capture.js +54 -0
- package/dist/runtime/use-funnel-flow-controller.d.ts +58 -5
- package/dist/runtime/use-funnel-flow-controller.js +804 -122
- package/dist/runtime/use-step-choices.d.ts +58 -0
- package/dist/runtime/use-step-choices.js +187 -0
- package/dist/sdk/userAnswers.d.ts +1 -0
- package/dist/services/api.service.d.ts +10 -0
- package/dist/services/api.service.js +32 -0
- package/dist/services/funnel-sdk.service.d.ts +17 -0
- package/dist/services/funnel-sdk.service.js +13 -0
- package/dist/steps/choice-contract.d.ts +33 -0
- package/dist/steps/choice-contract.js +302 -0
- package/dist/steps/step-contract.d.ts +498 -0
- package/dist/steps/step-contract.js +506 -0
- package/dist/steps/types.d.ts +7 -5
- package/dist/steps/types.js +1 -18
- package/dist/testing/funnel-contract-journey-driver.d.ts +35 -0
- package/dist/testing/funnel-contract-journey-driver.js +266 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/validation/fixtures/invalid-reserved-identities.d.ts +249 -0
- package/dist/validation/fixtures/invalid-reserved-identities.js +20 -0
- package/dist/validation/fixtures/valid-v2-project.d.ts +419 -0
- package/dist/validation/fixtures/valid-v2-project.js +228 -0
- package/dist/validation/fixtures/valid-v3-project.d.ts +419 -0
- package/dist/validation/fixtures/valid-v3-project.js +30 -0
- package/dist/validation/funnel-contract-diagnostics.d.ts +49 -0
- package/dist/validation/funnel-contract-diagnostics.js +67 -0
- package/dist/validation/funnel-contract-validator.d.ts +30 -0
- package/dist/validation/funnel-contract-validator.js +1458 -0
- package/dist/validation/funnel-manifest-input.normalization.d.ts +67 -0
- package/dist/validation/funnel-manifest-input.normalization.js +386 -0
- package/package.json +9 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FunnelStepId } from '../runtime/funnel-runtime.js';
|
|
2
2
|
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
3
|
+
import type { FunnelNavigationOutcome } from '../runtime/funnel-step-lifecycle.js';
|
|
3
4
|
/** A single record of a completed step, stored in the user's history. */
|
|
4
5
|
export type StepCompletionRecord = {
|
|
5
6
|
stepId: string;
|
|
@@ -20,7 +21,7 @@ export type FunnelContextValue = {
|
|
|
20
21
|
activeStepId: FunnelStepId;
|
|
21
22
|
featureFlags: Record<string, string>;
|
|
22
23
|
isBuilder: boolean;
|
|
23
|
-
goToStep: (stepId: string) => void;
|
|
24
|
+
goToStep: (stepId: string, outcome: FunnelNavigationOutcome) => void;
|
|
24
25
|
goNext: () => void;
|
|
25
26
|
goChoice: (choice: 'yes' | 'no', stepId?: string) => void;
|
|
26
27
|
getChoiceTargets: (stepId?: string) => {
|
|
@@ -35,8 +36,11 @@ export type FunnelContextValue = {
|
|
|
35
36
|
user: FunnelUser;
|
|
36
37
|
userBootstrapped: boolean;
|
|
37
38
|
setUser: (user: FunnelUser) => void;
|
|
39
|
+
submitEmailCapture: (email: string) => Promise<void>;
|
|
38
40
|
/** Manually record a step as completed (used by custom step components). */
|
|
39
41
|
completeStep: (stepId: string, choices?: Record<string, unknown>) => void;
|
|
42
|
+
/** Complete the active terminal step and emit the canonical funnel conversion once. */
|
|
43
|
+
completeFunnel: (stepId: string) => void;
|
|
40
44
|
};
|
|
41
45
|
type FunnelProviderProps = {
|
|
42
46
|
value: FunnelContextValue;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext } from 'react';
|
|
4
|
+
import { FunnelChoiceCommitProvider } from '../runtime/use-step-choices.js';
|
|
4
5
|
const FunnelContext = createContext(null);
|
|
5
6
|
export function FunnelProvider({ value, children }) {
|
|
6
|
-
return _jsx(FunnelContext.Provider, { value: value, children: children });
|
|
7
|
+
return (_jsx(FunnelChoiceCommitProvider, { carrier: value, children: _jsx(FunnelContext.Provider, { value: value, children: children }) }));
|
|
7
8
|
}
|
|
8
9
|
export function useFunnel() {
|
|
9
10
|
const context = useContext(FunnelContext);
|
|
@@ -152,7 +152,10 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
|
|
|
152
152
|
? content.confirmStage.cancellingLabel
|
|
153
153
|
: content.confirmStage.cancelLabel }), _jsx("button", { type: 'button', className: 'manage-subscription-secondary', onClick: () => setStage('why'), children: content.confirmStage.backLabel })] })) : null, stage === 'done' ? (_jsxs(_Fragment, { children: [_jsx("h1", { className: 'manage-subscription-title', children: content.doneStage.title }), _jsx("p", { className: 'manage-subscription-confirm-copy', children: cancelCompleted
|
|
154
154
|
? content.doneStage.cancelledMessage
|
|
155
|
-
: content.doneStage.emptyMessage }), _jsxs("p", { className: 'manage-subscription-support', children: [content.subscriptionsStage.supportPrefix, ' ', _jsx("a", { href: supportLinkHref, children: supportLinkLabel })] }), _jsx("button", { type: 'button', className: 'manage-subscription-primary', onClick: () => goToStep(homeStepId
|
|
155
|
+
: content.doneStage.emptyMessage }), _jsxs("p", { className: 'manage-subscription-support', children: [content.subscriptionsStage.supportPrefix, ' ', _jsx("a", { href: supportLinkHref, children: supportLinkLabel })] }), _jsx("button", { type: 'button', className: 'manage-subscription-primary', onClick: () => goToStep(homeStepId, {
|
|
156
|
+
type: 'exit',
|
|
157
|
+
reason: cancelCompleted ? 'cancel' : 'back',
|
|
158
|
+
}), children: content.doneStage.returnHomeLabel })] })) : null] }) }), _jsx("style", { children: manageSubscriptionScreenStyles })] }));
|
|
156
159
|
}
|
|
157
160
|
const manageSubscriptionScreenStyles = `
|
|
158
161
|
.manage-subscription-step {
|
|
@@ -23,9 +23,10 @@ export type SubscriptionHandoffContent = {
|
|
|
23
23
|
};
|
|
24
24
|
type SubscriptionHandoffScreenProps = {
|
|
25
25
|
stepId: string;
|
|
26
|
+
completionMode?: 'step' | 'funnel';
|
|
26
27
|
content: SubscriptionHandoffContent;
|
|
27
28
|
confirmationUrl?: string | null;
|
|
28
29
|
nodeId?: string;
|
|
29
30
|
};
|
|
30
|
-
export declare function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, nodeId, }: SubscriptionHandoffScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export declare function SubscriptionHandoffScreen({ stepId, completionMode, content, confirmationUrl, nodeId, }: SubscriptionHandoffScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
31
32
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import { getFunnelUserAttribution } from '../runtime/funnel-attribution.js';
|
|
5
5
|
import { resolveSubscriptionHandoff } from '../runtime/subscription-handoff.js';
|
|
6
6
|
import { runtimePublicConfig } from '../services/public-env.js';
|
|
@@ -43,8 +43,9 @@ const renderStoreIcon = (variant) => {
|
|
|
43
43
|
}
|
|
44
44
|
return _jsx(WebLinkIcon, {});
|
|
45
45
|
};
|
|
46
|
-
export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, nodeId, }) {
|
|
47
|
-
const { isBuilder, user, userBootstrapped } = useFunnel();
|
|
46
|
+
export function SubscriptionHandoffScreen({ stepId, completionMode = 'step', content, confirmationUrl, nodeId, }) {
|
|
47
|
+
const { completeFunnel, completeStep, isBuilder, user, userBootstrapped } = useFunnel();
|
|
48
|
+
const completeHandoff = useCallback(() => completionMode === 'funnel' ? completeFunnel(stepId) : completeStep(stepId), [completeFunnel, completeStep, completionMode, stepId]);
|
|
48
49
|
const [autoOpenAttempted, setAutoOpenAttempted] = useState(false);
|
|
49
50
|
const [emailSent, setEmailSent] = useState(false);
|
|
50
51
|
const browserSnapshotKey = useSyncExternalStore(subscribeToBrowserSnapshot, () => [
|
|
@@ -173,18 +174,22 @@ export function SubscriptionHandoffScreen({ stepId, content, confirmationUrl, no
|
|
|
173
174
|
return;
|
|
174
175
|
}
|
|
175
176
|
window.sessionStorage.setItem(attemptKey, '1');
|
|
177
|
+
completeHandoff();
|
|
176
178
|
window.location.assign(openAppUrl);
|
|
177
179
|
const statusTimer = window.setTimeout(() => {
|
|
178
180
|
setAutoOpenAttempted(true);
|
|
179
181
|
}, 0);
|
|
180
182
|
return () => window.clearTimeout(statusTimer);
|
|
181
|
-
}, [handoff.openAppUrl, handoff.platform, handoffReady, stepId, user.id]);
|
|
183
|
+
}, [completeHandoff, handoff.openAppUrl, handoff.platform, handoffReady, stepId, user.id]);
|
|
182
184
|
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
185
|
const label = `${link.eyebrow} ${link.title}`;
|
|
184
186
|
const className = `subscription-handoff-store-badge is-${link.variant}${link.disabled ? ' is-disabled' : ''}`;
|
|
185
187
|
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: () =>
|
|
188
|
+
return link.href ? (_jsx("a", { href: link.href, target: '_blank', rel: 'noopener noreferrer', "aria-label": label, className: className, onClick: completeHandoff, children: body }, link.id)) : (_jsx("span", { role: 'link', "aria-label": label, "aria-disabled": 'true', "aria-busy": 'true', className: className, children: body }, link.id));
|
|
189
|
+
}) })) : 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, onClick: completeHandoff, 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', onClick: completeHandoff, 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: () => {
|
|
190
|
+
completeHandoff();
|
|
191
|
+
setEmailSent(true);
|
|
192
|
+
}, 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 })] }));
|
|
188
193
|
}
|
|
189
194
|
const subscriptionHandoffScreenStyles = `
|
|
190
195
|
.subscription-handoff-step {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FunnelStepKind, FunnelStepType } from '../steps/
|
|
1
|
+
import type { FunnelChoiceDescriptor, FunnelStepKind, FunnelStepType } from '../steps/step-contract.js';
|
|
2
2
|
export type TemplateArchitectureVersion = number;
|
|
3
3
|
export type FunnelBreakpointName = 'small' | 'medium' | 'large' | 'desktop-small';
|
|
4
4
|
export type FunnelManifestBreakpoint = {
|
|
@@ -31,7 +31,8 @@ export type FunnelManifestStep = {
|
|
|
31
31
|
componentKey: string;
|
|
32
32
|
type: FunnelStepType;
|
|
33
33
|
kind?: FunnelStepKind;
|
|
34
|
-
name
|
|
34
|
+
name: string;
|
|
35
|
+
choice?: FunnelChoiceDescriptor;
|
|
35
36
|
title: string;
|
|
36
37
|
tags?: readonly string[];
|
|
37
38
|
assetIds?: readonly string[];
|
|
@@ -75,6 +76,7 @@ export type FunnelManifestExperiment = {
|
|
|
75
76
|
};
|
|
76
77
|
export type FunnelManifest = {
|
|
77
78
|
templateArchitectureVersion: TemplateArchitectureVersion;
|
|
79
|
+
stepContractVersion: number;
|
|
78
80
|
meta: {
|
|
79
81
|
title: string;
|
|
80
82
|
description: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CURRENT_STEP_CONTRACT_HASH: "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf";
|
package/dist/index.d.ts
CHANGED
|
@@ -5,18 +5,26 @@ export * from './config/funnel.experiments.types.js';
|
|
|
5
5
|
export * from './config/funnel-theme.js';
|
|
6
6
|
export * from './config/font-config.js';
|
|
7
7
|
export * from './content/step-content.js';
|
|
8
|
+
export * from './generated/step-contract-hash.js';
|
|
9
|
+
export * from './migrations/step-contract-v2.js';
|
|
10
|
+
export * from './migrations/step-contract-v3.js';
|
|
8
11
|
export * from './runtime/experiment-assignment.js';
|
|
9
12
|
export * from './runtime/browser-helpers.js';
|
|
10
13
|
export * from './runtime/funnel-flow.js';
|
|
11
14
|
export * from './runtime/funnel-attribution.js';
|
|
12
15
|
export * from './runtime/funnel-manifest.validation.js';
|
|
16
|
+
export * from './runtime/funnel-step-metadata.validation.js';
|
|
13
17
|
export * from './runtime/funnel-runtime.js';
|
|
18
|
+
export type { FunnelNavigationOutcome, FunnelStepExitReason, } from './runtime/funnel-step-lifecycle.js';
|
|
14
19
|
export * from './runtime/posthog-flags.js';
|
|
15
20
|
export * from './runtime/use-funnel-flow-controller.js';
|
|
21
|
+
export { useStepChoices } from './runtime/use-step-choices.js';
|
|
22
|
+
export type { FunnelStepChoices } from './runtime/use-step-choices.js';
|
|
16
23
|
export * from './runtime/preview-bridge.js';
|
|
17
24
|
export * from './runtime/preview-definition-overrides.js';
|
|
18
25
|
export * from './runtime/offer-set-runtime.js';
|
|
19
26
|
export * from './runtime/route-resolver.js';
|
|
27
|
+
export * from './runtime/submit-email-capture.js';
|
|
20
28
|
export * from './runtime/subscription-handoff.js';
|
|
21
29
|
export * from './runtime/url-user-attributes.js';
|
|
22
30
|
export * from './runtime/use-url-user-attributes-sync.js';
|
|
@@ -36,4 +44,8 @@ export * from './components/ManageSubscriptionScreen.js';
|
|
|
36
44
|
export * from './components/RuntimeDevInfoBox.js';
|
|
37
45
|
export * from './components/SubscriptionHandoffScreen.js';
|
|
38
46
|
export * from './components/shared/PrimaryButton.js';
|
|
47
|
+
export * from './steps/choice-contract.js';
|
|
48
|
+
export * from './steps/step-contract.js';
|
|
39
49
|
export * from './steps/types.js';
|
|
50
|
+
export * from './validation/funnel-contract-diagnostics.js';
|
|
51
|
+
export * from './validation/funnel-contract-validator.js';
|
package/dist/index.js
CHANGED
|
@@ -8,18 +8,24 @@ export * from './config/font-config.js';
|
|
|
8
8
|
// Structured content contracts.
|
|
9
9
|
export * from './content/step-content.js';
|
|
10
10
|
// Runtime mechanics.
|
|
11
|
+
export * from './generated/step-contract-hash.js';
|
|
12
|
+
export * from './migrations/step-contract-v2.js';
|
|
13
|
+
export * from './migrations/step-contract-v3.js';
|
|
11
14
|
export * from './runtime/experiment-assignment.js';
|
|
12
15
|
export * from './runtime/browser-helpers.js';
|
|
13
16
|
export * from './runtime/funnel-flow.js';
|
|
14
17
|
export * from './runtime/funnel-attribution.js';
|
|
15
18
|
export * from './runtime/funnel-manifest.validation.js';
|
|
19
|
+
export * from './runtime/funnel-step-metadata.validation.js';
|
|
16
20
|
export * from './runtime/funnel-runtime.js';
|
|
17
21
|
export * from './runtime/posthog-flags.js';
|
|
18
22
|
export * from './runtime/use-funnel-flow-controller.js';
|
|
23
|
+
export { useStepChoices } from './runtime/use-step-choices.js';
|
|
19
24
|
export * from './runtime/preview-bridge.js';
|
|
20
25
|
export * from './runtime/preview-definition-overrides.js';
|
|
21
26
|
export * from './runtime/offer-set-runtime.js';
|
|
22
27
|
export * from './runtime/route-resolver.js';
|
|
28
|
+
export * from './runtime/submit-email-capture.js';
|
|
23
29
|
export * from './runtime/subscription-handoff.js';
|
|
24
30
|
export * from './runtime/url-user-attributes.js';
|
|
25
31
|
export * from './runtime/use-url-user-attributes-sync.js';
|
|
@@ -41,4 +47,8 @@ export * from './components/ManageSubscriptionScreen.js';
|
|
|
41
47
|
export * from './components/RuntimeDevInfoBox.js';
|
|
42
48
|
export * from './components/SubscriptionHandoffScreen.js';
|
|
43
49
|
export * from './components/shared/PrimaryButton.js';
|
|
50
|
+
export * from './steps/choice-contract.js';
|
|
51
|
+
export * from './steps/step-contract.js';
|
|
44
52
|
export * from './steps/types.js';
|
|
53
|
+
export * from './validation/funnel-contract-diagnostics.js';
|
|
54
|
+
export * from './validation/funnel-contract-validator.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type FunnelContractDiagnostic } from '../validation/funnel-contract-diagnostics.js';
|
|
2
|
+
export type StepContractV2MigrationSourceKind = 'manifest' | 'component';
|
|
3
|
+
export type StepContractV2MigrationField = 'id' | 'name' | 'type' | 'kind' | 'choice';
|
|
4
|
+
export type StepContractV2MigrationRule = 'remove-default-kind' | 'add-name-from-id' | 'restore-reserved-step' | 'sync-manifest-from-component' | 'sync-component-from-manifest';
|
|
5
|
+
export type StepContractV2NormalizedMetadata = Readonly<{
|
|
6
|
+
id: unknown;
|
|
7
|
+
name: unknown;
|
|
8
|
+
type: unknown;
|
|
9
|
+
kind: unknown;
|
|
10
|
+
choice: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
export type StepContractV2UniqueMetadataSource = {
|
|
13
|
+
status: 'unique';
|
|
14
|
+
sourceId: string;
|
|
15
|
+
file: string;
|
|
16
|
+
metadata: StepContractV2NormalizedMetadata;
|
|
17
|
+
};
|
|
18
|
+
export type StepContractV2AmbiguousMetadataSource = {
|
|
19
|
+
status: 'computed' | 'spread' | 'dynamic' | 'multiple' | 'missing';
|
|
20
|
+
sourceId: string;
|
|
21
|
+
file: string;
|
|
22
|
+
};
|
|
23
|
+
export type StepContractV2MetadataSource = StepContractV2UniqueMetadataSource | StepContractV2AmbiguousMetadataSource;
|
|
24
|
+
export type StepContractV2MigrationStepInput = {
|
|
25
|
+
stepKey: string;
|
|
26
|
+
manifest: StepContractV2MetadataSource;
|
|
27
|
+
component: StepContractV2MetadataSource;
|
|
28
|
+
optionIds: 'stable' | 'ambiguous' | 'not-applicable';
|
|
29
|
+
};
|
|
30
|
+
export type StepContractV2MigrationInput = {
|
|
31
|
+
steps: readonly StepContractV2MigrationStepInput[];
|
|
32
|
+
};
|
|
33
|
+
export type StepContractV2MigrationOperation = {
|
|
34
|
+
source: StepContractV2MigrationSourceKind;
|
|
35
|
+
sourceId: string;
|
|
36
|
+
stepKey: string;
|
|
37
|
+
stepId: string | null;
|
|
38
|
+
field: StepContractV2MigrationField;
|
|
39
|
+
action: 'set' | 'remove';
|
|
40
|
+
before: unknown;
|
|
41
|
+
after: unknown;
|
|
42
|
+
rule: StepContractV2MigrationRule;
|
|
43
|
+
};
|
|
44
|
+
export type StepContractV2MigrationPlan = {
|
|
45
|
+
operations: readonly StepContractV2MigrationOperation[];
|
|
46
|
+
protectedChanges: readonly StepContractV2MigrationOperation[];
|
|
47
|
+
diagnostics: readonly FunnelContractDiagnostic[];
|
|
48
|
+
valid: boolean;
|
|
49
|
+
};
|
|
50
|
+
export declare const planStepContractV2Migration: (input: StepContractV2MigrationInput) => StepContractV2MigrationPlan;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { resolveReservedStepContract, } from '../steps/step-contract.js';
|
|
2
|
+
import { FUNNEL_CHOICE_STEP_TYPES } from '../steps/choice-contract.js';
|
|
3
|
+
import { createFunnelContractDiagnostic, } from '../validation/funnel-contract-diagnostics.js';
|
|
4
|
+
import { validateFunnelManifestContract } from '../validation/funnel-contract-validator.js';
|
|
5
|
+
const METADATA_FIELDS = ['id', 'name', 'type', 'kind', 'choice'];
|
|
6
|
+
const PROTECTED_FIELDS = new Set([
|
|
7
|
+
'id',
|
|
8
|
+
'name',
|
|
9
|
+
'type',
|
|
10
|
+
'kind',
|
|
11
|
+
]);
|
|
12
|
+
const KEBAB_CASE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
const AUTHORING_GUIDE = 'docs/product-specs/funnel-template-step-authoring.md';
|
|
14
|
+
const CHOICE_STEP_TYPE_SET = new Set(FUNNEL_CHOICE_STEP_TYPES);
|
|
15
|
+
const SAFE_RESERVED_CONTRACTS = [
|
|
16
|
+
'paywall',
|
|
17
|
+
'subscription-started',
|
|
18
|
+
'manage-subscription',
|
|
19
|
+
].map((identity) => resolveReservedStepContract(2, identity)).filter((contract) => (contract === null || contract === void 0 ? void 0 : contract.match) === 'exact');
|
|
20
|
+
const copyMetadata = (metadata) => ({
|
|
21
|
+
id: metadata.id,
|
|
22
|
+
name: metadata.name,
|
|
23
|
+
type: metadata.type,
|
|
24
|
+
kind: metadata.kind,
|
|
25
|
+
choice: metadata.choice,
|
|
26
|
+
});
|
|
27
|
+
const createPlannedSource = (source, input) => {
|
|
28
|
+
const before = copyMetadata(input.metadata);
|
|
29
|
+
return {
|
|
30
|
+
source,
|
|
31
|
+
input,
|
|
32
|
+
before,
|
|
33
|
+
after: Object.assign({}, before),
|
|
34
|
+
ruleByField: {},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const setPlannedField = (planned, field, value, rule) => {
|
|
38
|
+
if (planned.after[field] === value) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
planned.after[field] = value;
|
|
42
|
+
planned.ruleByField[field] = rule;
|
|
43
|
+
};
|
|
44
|
+
const choicesMatch = (left, right) => {
|
|
45
|
+
if (left === undefined || right === undefined) {
|
|
46
|
+
return left === right;
|
|
47
|
+
}
|
|
48
|
+
if (typeof left !== 'object'
|
|
49
|
+
|| left === null
|
|
50
|
+
|| Array.isArray(left)
|
|
51
|
+
|| typeof right !== 'object'
|
|
52
|
+
|| right === null
|
|
53
|
+
|| Array.isArray(right)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const leftChoice = left;
|
|
57
|
+
const rightChoice = right;
|
|
58
|
+
return leftChoice.answerKey === rightChoice.answerKey
|
|
59
|
+
&& leftChoice.allowEmpty === rightChoice.allowEmpty
|
|
60
|
+
&& Object.keys(leftChoice).length === Object.keys(rightChoice).length;
|
|
61
|
+
};
|
|
62
|
+
const metadataMatches = (left, right) => {
|
|
63
|
+
return left.id === right.id
|
|
64
|
+
&& left.name === right.name
|
|
65
|
+
&& left.type === right.type
|
|
66
|
+
&& left.kind === right.kind
|
|
67
|
+
&& choicesMatch(left.choice, right.choice);
|
|
68
|
+
};
|
|
69
|
+
const inspectMetadata = (metadata) => {
|
|
70
|
+
const id = typeof metadata.id === 'string' ? metadata.id : 'migration-candidate';
|
|
71
|
+
const diagnostics = validateFunnelManifestContract({
|
|
72
|
+
stepContractVersion: 2,
|
|
73
|
+
templateArchitectureVersion: 1,
|
|
74
|
+
steps: [{
|
|
75
|
+
id: metadata.id,
|
|
76
|
+
name: metadata.name,
|
|
77
|
+
type: metadata.type,
|
|
78
|
+
kind: metadata.kind,
|
|
79
|
+
choice: metadata.choice,
|
|
80
|
+
path: `/${id}`,
|
|
81
|
+
filePath: 'src/steps/migration-candidate.tsx',
|
|
82
|
+
componentKey: 'migrationCandidate',
|
|
83
|
+
title: 'Migration candidate',
|
|
84
|
+
}],
|
|
85
|
+
entryPoints: [],
|
|
86
|
+
edgesByStepId: {},
|
|
87
|
+
branches: [],
|
|
88
|
+
experiments: [],
|
|
89
|
+
}, { mode: 'legacy-read' }).filter((diagnostic) => diagnostic.code !== 'FG-RESERVED-002');
|
|
90
|
+
const choiceValid = diagnostics.every((diagnostic) => diagnostic.code !== 'FG-CHOICE-001');
|
|
91
|
+
const issue = diagnostics.find((diagnostic) => diagnostic.code !== 'FG-CHOICE-001');
|
|
92
|
+
return {
|
|
93
|
+
valid: diagnostics.length === 0,
|
|
94
|
+
choiceValid,
|
|
95
|
+
issue: issue ? {
|
|
96
|
+
code: issue.code,
|
|
97
|
+
expected: issue.expected,
|
|
98
|
+
received: issue.received,
|
|
99
|
+
reason: issue.reason,
|
|
100
|
+
repair: issue.repair,
|
|
101
|
+
} : null,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const applyMechanicalRules = (manifest, component) => {
|
|
105
|
+
const sharedId = manifest.after.id;
|
|
106
|
+
if (typeof sharedId === 'string'
|
|
107
|
+
&& sharedId === component.after.id
|
|
108
|
+
&& KEBAB_CASE_PATTERN.test(sharedId)) {
|
|
109
|
+
for (const source of [manifest, component]) {
|
|
110
|
+
if (source.after.name === undefined) {
|
|
111
|
+
setPlannedField(source, 'name', sharedId, 'add-name-from-id');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const source of [manifest, component]) {
|
|
116
|
+
const reserved = SAFE_RESERVED_CONTRACTS.find((candidate) => {
|
|
117
|
+
return source.after.id === candidate.id && source.after.name === candidate.name;
|
|
118
|
+
});
|
|
119
|
+
if (reserved) {
|
|
120
|
+
setPlannedField(source, 'type', reserved.type, 'restore-reserved-step');
|
|
121
|
+
setPlannedField(source, 'kind', reserved.kind, 'restore-reserved-step');
|
|
122
|
+
}
|
|
123
|
+
else if (source.after.kind === 'default') {
|
|
124
|
+
setPlannedField(source, 'kind', undefined, 'remove-default-kind');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const applyParityRule = (manifest, component) => {
|
|
129
|
+
if (metadataMatches(manifest.after, component.after)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const manifestValid = inspectMetadata(manifest.after).valid;
|
|
133
|
+
const componentValid = inspectMetadata(component.after).valid;
|
|
134
|
+
if (manifestValid === componentValid
|
|
135
|
+
|| manifest.after.id !== component.after.id
|
|
136
|
+
|| manifest.after.name !== component.after.name) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const source = manifestValid ? manifest : component;
|
|
140
|
+
const target = manifestValid ? component : manifest;
|
|
141
|
+
const rule = manifestValid
|
|
142
|
+
? 'sync-component-from-manifest'
|
|
143
|
+
: 'sync-manifest-from-component';
|
|
144
|
+
for (const field of ['type', 'kind']) {
|
|
145
|
+
if (target.after[field] !== source.after[field]) {
|
|
146
|
+
setPlannedField(target, field, source.after[field], rule);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const buildOperations = (stepKey, plannedSources) => {
|
|
151
|
+
const operations = [];
|
|
152
|
+
for (const planned of plannedSources) {
|
|
153
|
+
for (const field of METADATA_FIELDS) {
|
|
154
|
+
const rule = planned.ruleByField[field];
|
|
155
|
+
if (!rule || planned.before[field] === planned.after[field]) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
operations.push({
|
|
159
|
+
source: planned.source,
|
|
160
|
+
sourceId: planned.input.sourceId,
|
|
161
|
+
stepKey,
|
|
162
|
+
stepId: typeof planned.after.id === 'string' ? planned.after.id : null,
|
|
163
|
+
field,
|
|
164
|
+
action: planned.after[field] === undefined ? 'remove' : 'set',
|
|
165
|
+
before: planned.before[field],
|
|
166
|
+
after: planned.after[field],
|
|
167
|
+
rule,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return operations;
|
|
172
|
+
};
|
|
173
|
+
const createMigrationDiagnostic = (details, file, stepId) => {
|
|
174
|
+
return createFunnelContractDiagnostic(Object.assign(Object.assign({}, details), { file,
|
|
175
|
+
stepId, guide: AUTHORING_GUIDE }));
|
|
176
|
+
};
|
|
177
|
+
const createSourceDiagnostic = (source, input) => {
|
|
178
|
+
return createMigrationDiagnostic({
|
|
179
|
+
code: 'FG-CANDIDATE-001',
|
|
180
|
+
expected: 'one static object-literal metadata match',
|
|
181
|
+
received: { source, status: input.status },
|
|
182
|
+
reason: `The ${source} metadata source cannot be located without evaluating or guessing source code.`,
|
|
183
|
+
repair: 'Replace computed, spread, dynamic, missing, or duplicate metadata with one static object literal.',
|
|
184
|
+
}, input.file, null);
|
|
185
|
+
};
|
|
186
|
+
const finalStepId = (manifest, component) => {
|
|
187
|
+
if (typeof manifest.after.id === 'string') {
|
|
188
|
+
return manifest.after.id;
|
|
189
|
+
}
|
|
190
|
+
return typeof component.after.id === 'string' ? component.after.id : null;
|
|
191
|
+
};
|
|
192
|
+
const createChoiceDiagnostic = (manifest, component) => {
|
|
193
|
+
return createMigrationDiagnostic({
|
|
194
|
+
code: 'FG-CHOICE-001',
|
|
195
|
+
expected: 'one unchanged valid choice descriptor on both metadata sides',
|
|
196
|
+
received: {
|
|
197
|
+
manifest: manifest.after.choice,
|
|
198
|
+
component: component.after.choice,
|
|
199
|
+
},
|
|
200
|
+
reason: 'Choice answer-key metadata cannot be inferred or synchronized mechanically.',
|
|
201
|
+
repair: 'Declare and preserve the stable answerKey explicitly on both metadata sides.',
|
|
202
|
+
}, manifest.input.file, finalStepId(manifest, component));
|
|
203
|
+
};
|
|
204
|
+
const createOptionDiagnostic = (manifest, component, expected, received) => {
|
|
205
|
+
return createMigrationDiagnostic({
|
|
206
|
+
code: 'FG-CHOICE-002',
|
|
207
|
+
expected,
|
|
208
|
+
received,
|
|
209
|
+
reason: 'Choice option identities or presentation shapes require an explicit decision.',
|
|
210
|
+
repair: 'Preserve stable option ids and resolve the option shape before migrating.',
|
|
211
|
+
}, manifest.input.file, finalStepId(manifest, component));
|
|
212
|
+
};
|
|
213
|
+
const optionEvidenceDiagnostic = (step, manifest, component) => {
|
|
214
|
+
const evidence = step.optionIds;
|
|
215
|
+
const allowedEvidence = ['stable', 'ambiguous', 'not-applicable'];
|
|
216
|
+
if (!allowedEvidence.some((value) => value === evidence)) {
|
|
217
|
+
return createOptionDiagnostic(manifest, component, allowedEvidence, evidence === undefined ? 'missing' : evidence);
|
|
218
|
+
}
|
|
219
|
+
const requiresStableOptions = [manifest.after.type, component.after.type].some((type) => typeof type === 'string' && CHOICE_STEP_TYPE_SET.has(type));
|
|
220
|
+
if (requiresStableOptions && evidence !== 'stable') {
|
|
221
|
+
return createOptionDiagnostic(manifest, component, 'stable', evidence);
|
|
222
|
+
}
|
|
223
|
+
if (evidence === 'ambiguous') {
|
|
224
|
+
return createOptionDiagnostic(manifest, component, ['stable', 'not-applicable'], evidence);
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
};
|
|
228
|
+
const createParityDiagnostics = (manifest, component) => {
|
|
229
|
+
const diagnostics = [];
|
|
230
|
+
for (const field of ['id', 'name', 'type', 'kind']) {
|
|
231
|
+
if (manifest.after[field] === component.after[field]) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
diagnostics.push(createMigrationDiagnostic({
|
|
235
|
+
code: 'FG-PARITY-001',
|
|
236
|
+
expected: manifest.after[field],
|
|
237
|
+
received: component.after[field],
|
|
238
|
+
reason: `Component metadata ${field} does not match the manifest step.`,
|
|
239
|
+
repair: `Resolve the protected ${field} explicitly before migrating.`,
|
|
240
|
+
}, component.input.file, finalStepId(manifest, component)));
|
|
241
|
+
}
|
|
242
|
+
return diagnostics;
|
|
243
|
+
};
|
|
244
|
+
const collectSemanticDiagnostics = (step, manifest, component) => {
|
|
245
|
+
var _a, _b, _c;
|
|
246
|
+
const diagnostics = [];
|
|
247
|
+
const inspectionBySource = new Map([
|
|
248
|
+
[manifest, inspectMetadata(manifest.after)],
|
|
249
|
+
[component, inspectMetadata(component.after)],
|
|
250
|
+
]);
|
|
251
|
+
if (!((_a = inspectionBySource.get(manifest)) === null || _a === void 0 ? void 0 : _a.choiceValid)
|
|
252
|
+
|| !((_b = inspectionBySource.get(component)) === null || _b === void 0 ? void 0 : _b.choiceValid)
|
|
253
|
+
|| !choicesMatch(manifest.after.choice, component.after.choice)) {
|
|
254
|
+
diagnostics.push(createChoiceDiagnostic(manifest, component));
|
|
255
|
+
}
|
|
256
|
+
const optionDiagnostic = optionEvidenceDiagnostic(step, manifest, component);
|
|
257
|
+
if (optionDiagnostic) {
|
|
258
|
+
diagnostics.push(optionDiagnostic);
|
|
259
|
+
}
|
|
260
|
+
diagnostics.push(...createParityDiagnostics(manifest, component));
|
|
261
|
+
const metadataSources = (manifest.after.id === component.after.id
|
|
262
|
+
&& manifest.after.name === component.after.name
|
|
263
|
+
&& manifest.after.type === component.after.type
|
|
264
|
+
&& manifest.after.kind === component.after.kind) ? [manifest] : [manifest, component];
|
|
265
|
+
const issueKeys = new Set();
|
|
266
|
+
for (const source of metadataSources) {
|
|
267
|
+
const issue = (_c = inspectionBySource.get(source)) === null || _c === void 0 ? void 0 : _c.issue;
|
|
268
|
+
if (!issue) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const issueKey = JSON.stringify({
|
|
272
|
+
code: issue.code,
|
|
273
|
+
expected: issue.expected,
|
|
274
|
+
received: issue.received,
|
|
275
|
+
});
|
|
276
|
+
if (issueKeys.has(issueKey)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
issueKeys.add(issueKey);
|
|
280
|
+
diagnostics.push(createMigrationDiagnostic(issue, source.input.file, typeof source.after.id === 'string' ? source.after.id : null));
|
|
281
|
+
}
|
|
282
|
+
return diagnostics;
|
|
283
|
+
};
|
|
284
|
+
export const planStepContractV2Migration = (input) => {
|
|
285
|
+
const operations = [];
|
|
286
|
+
const diagnostics = [];
|
|
287
|
+
for (const step of input.steps) {
|
|
288
|
+
if (step.manifest.status !== 'unique') {
|
|
289
|
+
diagnostics.push(createSourceDiagnostic('manifest', step.manifest));
|
|
290
|
+
}
|
|
291
|
+
if (step.component.status !== 'unique') {
|
|
292
|
+
diagnostics.push(createSourceDiagnostic('component', step.component));
|
|
293
|
+
}
|
|
294
|
+
if (step.manifest.status !== 'unique' || step.component.status !== 'unique') {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const manifest = createPlannedSource('manifest', step.manifest);
|
|
298
|
+
const component = createPlannedSource('component', step.component);
|
|
299
|
+
applyMechanicalRules(manifest, component);
|
|
300
|
+
applyParityRule(manifest, component);
|
|
301
|
+
operations.push(...buildOperations(step.stepKey, [manifest, component]));
|
|
302
|
+
diagnostics.push(...collectSemanticDiagnostics(step, manifest, component));
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
operations,
|
|
306
|
+
protectedChanges: operations.filter((operation) => PROTECTED_FIELDS.has(operation.field)),
|
|
307
|
+
diagnostics,
|
|
308
|
+
valid: diagnostics.length === 0,
|
|
309
|
+
};
|
|
310
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CURRENT_STEP_CONTRACT_VERSION } from '../steps/step-contract.js';
|
|
2
|
+
export type StepContractV3MigrationStep = Readonly<{
|
|
3
|
+
id?: unknown;
|
|
4
|
+
name?: unknown;
|
|
5
|
+
type: unknown;
|
|
6
|
+
kind?: unknown;
|
|
7
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}>;
|
|
10
|
+
export type StepContractV3MigrationManifest = Readonly<{
|
|
11
|
+
stepContractVersion: 2 | 3;
|
|
12
|
+
steps: readonly StepContractV3MigrationStep[];
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}>;
|
|
15
|
+
type StepContractV3MigratedMetadata<T> = T extends Readonly<Record<string, unknown>> ? T extends Readonly<{
|
|
16
|
+
stepType: 'complete_registration';
|
|
17
|
+
}> ? Omit<T, 'stepType'> & Readonly<{
|
|
18
|
+
stepType: 'purchase_completed';
|
|
19
|
+
}> : T : T;
|
|
20
|
+
type StepContractV3MigratedStep<T> = T extends Readonly<{
|
|
21
|
+
id: 'subscription-started';
|
|
22
|
+
name: 'subscription-started';
|
|
23
|
+
type: 'complete_registration';
|
|
24
|
+
kind: 'subscription-handoff';
|
|
25
|
+
}> ? Omit<T, 'type' | 'metadata'> & Readonly<{
|
|
26
|
+
type: 'purchase_completed';
|
|
27
|
+
}> & (T extends Readonly<{
|
|
28
|
+
metadata: infer M;
|
|
29
|
+
}> ? Readonly<{
|
|
30
|
+
metadata: StepContractV3MigratedMetadata<M>;
|
|
31
|
+
}> : unknown) : T;
|
|
32
|
+
type StepContractV3MigratedSteps<T extends readonly StepContractV3MigrationStep[]> = {
|
|
33
|
+
[K in keyof T]: StepContractV3MigratedStep<T[K]>;
|
|
34
|
+
};
|
|
35
|
+
export type StepContractV3MigratedManifest<T extends StepContractV3MigrationManifest> = T extends StepContractV3MigrationManifest ? Omit<T, 'stepContractVersion' | 'steps'> & Readonly<{
|
|
36
|
+
stepContractVersion: typeof CURRENT_STEP_CONTRACT_VERSION;
|
|
37
|
+
steps: T['stepContractVersion'] extends 2 ? StepContractV3MigratedSteps<T['steps']> : T['steps'];
|
|
38
|
+
}> : never;
|
|
39
|
+
export declare const isStepContractV2PurchaseTerminal: (step: Pick<StepContractV3MigrationStep, "id" | "name" | "type" | "kind">) => boolean;
|
|
40
|
+
export declare function migrateStepContractV2ToV3<T extends StepContractV3MigrationManifest>(manifest: T): StepContractV3MigratedManifest<T>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CURRENT_STEP_CONTRACT_VERSION, resolveReservedStepContract, } from '../steps/step-contract.js';
|
|
2
|
+
const PREVIOUS_TERMINAL_TYPE = 'complete_registration';
|
|
3
|
+
const CURRENT_TERMINAL_TYPE = 'purchase_completed';
|
|
4
|
+
export const isStepContractV2PurchaseTerminal = (step) => {
|
|
5
|
+
const reserved = resolveReservedStepContract(2, 'subscription-started');
|
|
6
|
+
return (reserved === null || reserved === void 0 ? void 0 : reserved.match) === 'exact'
|
|
7
|
+
&& step.id === reserved.id
|
|
8
|
+
&& step.name === reserved.name
|
|
9
|
+
&& step.type === reserved.type
|
|
10
|
+
&& step.kind === reserved.kind;
|
|
11
|
+
};
|
|
12
|
+
export function migrateStepContractV2ToV3(manifest) {
|
|
13
|
+
if (manifest.stepContractVersion === 3) {
|
|
14
|
+
return manifest;
|
|
15
|
+
}
|
|
16
|
+
const steps = manifest.steps.map((step) => {
|
|
17
|
+
var _a;
|
|
18
|
+
if (!isStepContractV2PurchaseTerminal(step)) {
|
|
19
|
+
return step;
|
|
20
|
+
}
|
|
21
|
+
const metadata = ((_a = step.metadata) === null || _a === void 0 ? void 0 : _a.stepType) === PREVIOUS_TERMINAL_TYPE
|
|
22
|
+
? Object.assign(Object.assign({}, step.metadata), { stepType: CURRENT_TERMINAL_TYPE }) : step.metadata;
|
|
23
|
+
return Object.assign(Object.assign(Object.assign({}, step), { type: CURRENT_TERMINAL_TYPE }), (metadata === undefined ? {} : { metadata }));
|
|
24
|
+
});
|
|
25
|
+
return Object.assign(Object.assign({}, manifest), { stepContractVersion: CURRENT_STEP_CONTRACT_VERSION, steps });
|
|
26
|
+
}
|