@funnelsgrove/cli 0.1.87 → 0.1.89

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 (121) hide show
  1. package/dist/apiClient.d.ts +1 -0
  2. package/dist/apiClient.js +32 -1
  3. package/funnel-contract-compatibility.json +28 -28
  4. package/package.json +3 -3
  5. package/template_docs/.funnelsgrove-docs.json +3 -3
  6. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  7. package/template_docs/funnel-docs.config.json +1 -1
  8. package/template_scaffold/.funnelsgrove-docs.json +3 -3
  9. package/template_scaffold/.funnelsgrove-scaffold.json +26 -471
  10. package/template_scaffold/PLAN.md +15 -29
  11. package/template_scaffold/context/build-context.md +3 -3
  12. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  13. package/template_scaffold/funnel-docs.config.json +1 -1
  14. package/template_scaffold/package-lock.json +9 -9
  15. package/template_scaffold/package.json +2 -2
  16. package/template_scaffold/public/assets/email-capture-preview.svg +5 -0
  17. package/template_scaffold/public/assets/paywall-preview.svg +6 -0
  18. package/template_scaffold/src/app/sdk/page.tsx +14 -26
  19. package/template_scaffold/src/components/FunnelFlow.tsx +8 -1
  20. package/template_scaffold/src/config/billing.plans.ts +71 -21
  21. package/template_scaffold/src/config/funnel.manifest.ts +20 -59
  22. package/template_scaffold/src/runtime/checkout-runtime-config.ts +0 -7
  23. package/template_scaffold/src/runtime/step-registry.ts +0 -28
  24. package/template_scaffold/src/steps/content/step-01.content.ts +4 -9
  25. package/template_scaffold/src/steps/content/step-32-paywall.content.ts +12 -50
  26. package/template_scaffold/src/steps/step-01.tsx +43 -50
  27. package/template_scaffold/src/steps/step-31-email-capture.tsx +18 -1
  28. package/template_scaffold/src/steps/step-32-paywall.tsx +93 -299
  29. package/template_scaffold/src/steps/step-33-subscription-started.tsx +1 -1
  30. package/template_scaffold/src/steps/step-content.registry.ts +1 -26
  31. package/template_scaffold/public/claimbee/bee.png +0 -0
  32. package/template_scaffold/public/claimbee/device-iphone.png +0 -0
  33. package/template_scaffold/public/claimbee/device-macbook.png +0 -0
  34. package/template_scaffold/public/claimbee/device-none.png +0 -0
  35. package/template_scaffold/public/claimbee/device-watch.png +0 -0
  36. package/template_scaffold/public/claimbee/glow.svg +0 -3
  37. package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
  38. package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
  39. package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
  40. package/template_scaffold/public/claimbee/testimonial-star.svg +0 -3
  41. package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
  42. package/template_scaffold/public/paywall/claim-history.png +0 -0
  43. package/template_scaffold/public/paywall/clear-progress.png +0 -0
  44. package/template_scaffold/public/paywall/hero.png +0 -0
  45. package/template_scaffold/public/paywall/money-alerts.png +0 -0
  46. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  47. package/template_scaffold/public/paywall/paywall-image.png +0 -0
  48. package/template_scaffold/public/paywall/r1.png +0 -0
  49. package/template_scaffold/public/paywall/r2.png +0 -0
  50. package/template_scaffold/public/paywall/r3.png +0 -0
  51. package/template_scaffold/public/paywall/r4.png +0 -0
  52. package/template_scaffold/public/paywall/r5.png +0 -0
  53. package/template_scaffold/public/paywall/r6.png +0 -0
  54. package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
  55. package/template_scaffold/public/please/bee.png +0 -0
  56. package/template_scaffold/public/please/device-iphone.png +0 -0
  57. package/template_scaffold/public/please/device-macbook.png +0 -0
  58. package/template_scaffold/public/please/device-none.png +0 -0
  59. package/template_scaffold/public/please/device-watch.png +0 -0
  60. package/template_scaffold/public/please/glow.svg +0 -3
  61. package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
  62. package/template_scaffold/public/please/steps-sprite-default.png +0 -0
  63. package/template_scaffold/public/please/testimonial-avatar.png +0 -0
  64. package/template_scaffold/public/please/testimonial-star.svg +0 -3
  65. package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
  66. package/template_scaffold/src/assets/claimbee/bee.png +0 -0
  67. package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
  68. package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
  69. package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
  70. package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
  71. package/template_scaffold/src/assets/claimbee/glow.svg +0 -3
  72. package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
  73. package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
  74. package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
  75. package/template_scaffold/src/assets/claimbee/testimonial-star.svg +0 -3
  76. package/template_scaffold/src/components/FunnelStepImage.tsx +0 -28
  77. package/template_scaffold/src/config/offer-sets.generated.ts +0 -4
  78. package/template_scaffold/src/steps/content/manage-subscription.content.ts +0 -62
  79. package/template_scaffold/src/steps/content/paywall.content.ts +0 -191
  80. package/template_scaffold/src/steps/content/step-02.content.ts +0 -77
  81. package/template_scaffold/src/steps/content/step-03.content.ts +0 -40
  82. package/template_scaffold/src/steps/content/step-04.content.ts +0 -49
  83. package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +0 -179
  84. package/template_scaffold/src/steps/editor/paywall.editor.ts +0 -232
  85. package/template_scaffold/src/steps/editor/step-02.editor.ts +0 -41
  86. package/template_scaffold/src/steps/editor/step-03.editor.ts +0 -58
  87. package/template_scaffold/src/steps/editor/step-04.editor.ts +0 -98
  88. package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +0 -12
  89. package/template_scaffold/src/steps/step-02.tsx +0 -140
  90. package/template_scaffold/src/steps/step-03.tsx +0 -262
  91. package/template_scaffold/src/steps/step-04.tsx +0 -673
  92. package/template_scaffold/src/steps/step-32b-paywall-b.tsx +0 -35
  93. package/template_scaffold/src/steps/step-35-manage-subscription.tsx +0 -46
  94. package/template_scaffold/src/steps/use-claimbee-step-content.ts +0 -15
  95. package/template_scaffold/tests/funnel-agent-docs.test.ts +0 -751
  96. package/template_scaffold/tests/package.test.ts +0 -37
  97. package/template_scaffold/tests/src/app/[stepId]/page.test.tsx +0 -59
  98. package/template_scaffold/tests/src/app/layout.test.ts +0 -26
  99. package/template_scaffold/tests/src/components/FunnelFlow.test.tsx +0 -38
  100. package/template_scaffold/tests/src/components/FunnelStepImage.test.ts +0 -15
  101. package/template_scaffold/tests/src/config/billing.plans.test.ts +0 -143
  102. package/template_scaffold/tests/src/config/funnel.manifest.test.ts +0 -166
  103. package/template_scaffold/tests/src/contract/funnel-project.validation.test.ts +0 -79
  104. package/template_scaffold/tests/src/contract/golden/golden-fixtures.test.ts +0 -190
  105. package/template_scaffold/tests/src/contract/validate-funnel.cli.test.ts +0 -191
  106. package/template_scaffold/tests/src/runtime/checkout-runtime-config.test.ts +0 -60
  107. package/template_scaffold/tests/src/runtime/funnel-runtime.test.ts +0 -25
  108. package/template_scaffold/tests/src/runtime/step-content-context.test.ts +0 -38
  109. package/template_scaffold/tests/src/runtime/use-funnel-flow-controller.integration.test.ts +0 -203
  110. package/template_scaffold/tests/src/runtime/use-funnel-flow-controller.test.ts +0 -76
  111. package/template_scaffold/tests/src/steps/paywall-checkout-options.test.ts +0 -104
  112. package/template_scaffold/tests/src/steps/shared-header-contract.test.ts +0 -29
  113. package/template_scaffold/tests/src/steps/step-01.test.ts +0 -38
  114. package/template_scaffold/tests/src/steps/step-02.test.ts +0 -43
  115. package/template_scaffold/tests/src/steps/step-03.test.ts +0 -15
  116. package/template_scaffold/tests/src/steps/step-04.test.ts +0 -32
  117. package/template_scaffold/tests/src/steps/step-31-email-capture.test.ts +0 -22
  118. package/template_scaffold/tests/src/steps/step-32-paywall.test.ts +0 -773
  119. package/template_scaffold/tests/src/steps/step-33-subscription-started.test.ts +0 -44
  120. package/template_scaffold/tests/src/steps/step-34-upsell-form.test.ts +0 -27
  121. package/template_scaffold/tests/src/steps/step-image-migration.test.ts +0 -20
@@ -1,37 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { describe, expect, it } from 'vitest';
5
-
6
- type PackageJson = {
7
- dependencies?: Record<string, string>;
8
- version?: string;
9
- };
10
-
11
- const readPackageJson = (path: URL | string): PackageJson =>
12
- JSON.parse(readFileSync(path, 'utf8')) as PackageJson;
13
-
14
- const findRepositoryRoot = (): string => {
15
- let current = dirname(fileURLToPath(import.meta.url));
16
- while (dirname(current) !== current) {
17
- if (existsSync(join(current, 'apps/funnel-runtime/package.json'))) {
18
- return current;
19
- }
20
- current = dirname(current);
21
- }
22
- throw new Error('FunnelsGrove repository root not found');
23
- };
24
-
25
- describe('funnel-template shared package dependencies', () => {
26
- it('tracks the current shared runtime, analytics, and payments package versions', () => {
27
- const templatePackage = readPackageJson(new URL('../package.json', import.meta.url));
28
- const repositoryRoot = findRepositoryRoot();
29
- const runtimePackage = readPackageJson(join(repositoryRoot, 'apps/funnel-runtime/package.json'));
30
- const analyticsPackage = readPackageJson(join(repositoryRoot, 'apps/funnel-analytics/package.json'));
31
- const paymentsPackage = readPackageJson(join(repositoryRoot, 'apps/funnel-payments/package.json'));
32
-
33
- expect(templatePackage.dependencies?.['@funnelsgrove/runtime']).toBe(`^${runtimePackage.version}`);
34
- expect(templatePackage.dependencies?.['@funnelsgrove/analytics']).toBe(`^${analyticsPackage.version}`);
35
- expect(templatePackage.dependencies?.['@funnelsgrove/payments']).toBe(`^${paymentsPackage.version}`);
36
- });
37
- });
@@ -1,59 +0,0 @@
1
- import { vi, describe, expect, it, beforeEach } from 'vitest';
2
-
3
- const mockRedirect = vi.fn();
4
-
5
- vi.mock('next/navigation', () => ({
6
- redirect: (target: string) => {
7
- mockRedirect(target);
8
- throw new Error(`redirect:${target}`);
9
- },
10
- }));
11
-
12
- vi.mock('@/runtime/funnel-runtime', () => ({
13
- defaultFunnelStepId: 'step-1',
14
- funnelStepSequence: ['step-1', 'step-2'],
15
- getPathForStep: (stepId: string) => `/${stepId}`,
16
- isFunnelStepId: (stepId: string) => stepId === 'step-1' || stepId === 'step-2',
17
- }));
18
-
19
- describe('step preview page', () => {
20
- beforeEach(() => {
21
- mockRedirect.mockReset();
22
- });
23
-
24
- it('passes thumbnail preview mode through without top padding', async () => {
25
- const StepPageModule = await import('@/app/[stepId]/page');
26
-
27
- const page = await StepPageModule.default({
28
- params: Promise.resolve({ stepId: 'step-1' }),
29
- searchParams: Promise.resolve({
30
- previewFrame: '1',
31
- previewStepLock: '1',
32
- previewThumbnail: '1',
33
- }),
34
- });
35
-
36
- expect(page.props.style).toEqual({
37
- justifyContent: 'flex-start',
38
- });
39
- expect(page.props.children.props).toEqual({
40
- initialStepId: 'step-1',
41
- lockToInitialStep: true,
42
- isPreviewFrame: true,
43
- isPreviewThumbnail: true,
44
- });
45
- });
46
-
47
- it('redirects unknown steps to the first manifest step', async () => {
48
- const StepPageModule = await import('@/app/[stepId]/page');
49
-
50
- await expect(
51
- StepPageModule.default({
52
- params: Promise.resolve({ stepId: 'missing-step' }),
53
- searchParams: Promise.resolve({}),
54
- }),
55
- ).rejects.toThrow('redirect:/step-1');
56
-
57
- expect(mockRedirect).toHaveBeenCalledWith('/step-1');
58
- });
59
- });
@@ -1,26 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { describe, expect, it } from 'vitest';
4
-
5
- describe('RootLayout metadata source', () => {
6
- it('wires project page metadata settings into Next metadata', () => {
7
- const layoutSource = readFileSync(path.resolve(__dirname, '../../../src/app/layout.tsx'), 'utf8');
8
-
9
- expect(layoutSource).toContain('runtimePublicConfig.pageTitle');
10
- expect(layoutSource).toContain('runtimePublicConfig.pageDescription');
11
- expect(layoutSource).toContain('runtimePublicConfig.faviconUrl');
12
- expect(layoutSource).toContain('runtimePublicConfig.pagePreviewImageUrl');
13
- expect(layoutSource).toContain('openGraph');
14
- expect(layoutSource).toContain('twitter');
15
- });
16
-
17
- it('renders Google tag head scripts through the shared analytics package', () => {
18
- const layoutSource = readFileSync(path.resolve(__dirname, '../../../src/app/layout.tsx'), 'utf8');
19
-
20
- expect(layoutSource).toContain("import { buildGoogleTagHeadScripts } from '@funnelsgrove/analytics'");
21
- expect(layoutSource).toContain('const googleTagHeadScripts = buildGoogleTagHeadScripts();');
22
- expect(layoutSource).toContain('<head>');
23
- expect(layoutSource).toContain('googleTagHeadScripts.map');
24
- expect(layoutSource).toContain('dangerouslySetInnerHTML');
25
- });
26
- });
@@ -1,38 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- describe('FunnelFlow lifecycle wiring', () => {
5
- it('delegates one analytics adapter and the authoritative manifest version to the shared controller', () => {
6
- const flowSource = readFileSync(new URL('../../../src/components/FunnelFlow.tsx', import.meta.url), 'utf8');
7
- const adapterSource = readFileSync(
8
- new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url),
9
- 'utf8',
10
- );
11
-
12
- expect(flowSource).toContain('const controller = useFunnelFlowController({');
13
- expect(flowSource).not.toContain('publicAnalyticsSdk');
14
- expect(adapterSource.match(/analytics: publicAnalyticsSdk/g)).toHaveLength(1);
15
- expect(adapterSource.match(/stepContractVersion: funnelManifest\.stepContractVersion/g)).toHaveLength(1);
16
- expect(flowSource).toContain('<FunnelRuntimeConfigBoundary');
17
- expect(flowSource).toContain('disabled={isPreviewRuntime}');
18
- expect(adapterSource).toContain('toManifestExperiments(publishedRuntime.config.experiments)');
19
- expect(adapterSource).toContain(': funnelExperiments');
20
- });
21
-
22
- it('keeps A/B editor controls without wiring a manual payment mode switch', () => {
23
- const flowSource = readFileSync(new URL('../../../src/components/FunnelFlow.tsx', import.meta.url), 'utf8');
24
- const panelSource = readFileSync(
25
- new URL('../../../src/components/FunnelEditorPanel.tsx', import.meta.url),
26
- 'utf8',
27
- );
28
-
29
- expect(flowSource).not.toContain('onRuntimeModeChange');
30
- expect(flowSource).not.toContain('runtimeMode={controller.runtimeMode}');
31
- expect(panelSource).toContain('A/B TESTS');
32
- expect(panelSource).not.toContain('RuntimeMode');
33
- expect(panelSource).not.toContain('funnel-editor-panel-options');
34
- expect(panelSource).not.toContain('>MODE<');
35
- expect(panelSource).not.toContain('>Test<');
36
- expect(panelSource).not.toContain('>Live<');
37
- });
38
- });
@@ -1,15 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { createFunnelStepImageLoader } from '@/components/FunnelStepImage';
3
-
4
- describe('FunnelStepImage', () => {
5
- it('keeps builder-friendly local and remote image src values unchanged', () => {
6
- const loader = createFunnelStepImageLoader();
7
-
8
- expect(loader({ src: '/claimbee/steps-sprite-default.png', width: 430, quality: 75 })).toBe(
9
- '/claimbee/steps-sprite-default.png',
10
- );
11
- expect(loader({ src: 'https://example.com/remote-step-image.png', width: 430, quality: 75 })).toBe(
12
- 'https://example.com/remote-step-image.png',
13
- );
14
- });
15
- });
@@ -1,143 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { buildConfigPaywallPlans } from '@funnelsgrove/payments';
3
-
4
- const CLAIMBEE_PROJECT_PLAN_IDS = {
5
- weekly: '32b31f69-5907-4736-89c7-76d3acb38813',
6
- monthly: 'ce0a0022-7bf0-41f2-a707-edc5e6113a07',
7
- yearly: 'fb623ebc-c2fd-4e6a-8eb6-b8ed92db1b21',
8
- } as const;
9
-
10
- const CLAIMBEE_TEST_PROVIDER_PLAN_IDS = {
11
- weekly: 'price_1TOQ2fPEfkEVo5khumapOSsI',
12
- monthly: 'price_1TSgPZPEfkEVo5khMbtAlqsG',
13
- yearly: 'price_1TBgcaPEfkEVo5khbGnGhrBA',
14
- } as const;
15
-
16
- const CLAIMBEE_LIVE_PROVIDER_PLAN_IDS = {
17
- weekly: 'price_1T9WL2PPRN41Ty94g2NNOgif',
18
- monthly: 'price_1TP0IIPPRN41Ty94npkzVeDR',
19
- yearly: 'price_1T06AEPPRN41Ty94zvVt5xrk',
20
- } as const;
21
-
22
- const stubRuntimeMode = (mode: 'test' | 'live') => {
23
- vi.stubGlobal('window', {
24
- location: {
25
- hostname: mode === 'test' ? 'claimbee-preview.funnelsgrove.org' : 'check.claimbee.app',
26
- search: mode === 'test' ? '?mode=test' : '?mode=prod',
27
- },
28
- localStorage: {
29
- getItem: vi.fn(() => null),
30
- setItem: vi.fn(),
31
- },
32
- dispatchEvent: vi.fn(),
33
- addEventListener: vi.fn(),
34
- removeEventListener: vi.fn(),
35
- });
36
- vi.stubGlobal('document', {
37
- cookie: '',
38
- });
39
- };
40
-
41
- describe('claimbee billing contract', () => {
42
- afterEach(() => {
43
- vi.unstubAllGlobals();
44
- vi.resetModules();
45
- });
46
-
47
- it('exports minimal live billing config and normalized discounts', async () => {
48
- const billingModule = await import('../../../src/config/billing.plans');
49
- const {
50
- billing,
51
- billingDiscountList,
52
- billingDiscounts,
53
- billingPlans,
54
- } = billingModule;
55
-
56
- expect(Object.keys(billing.plans)).toEqual(['primary', 'secondary', 'tertiary']);
57
- expect(billing.plans.primary).toEqual({
58
- projectPlanId: CLAIMBEE_PROJECT_PLAN_IDS.weekly,
59
- title: '1-Week Plan',
60
- providerPlanId: CLAIMBEE_LIVE_PROVIDER_PLAN_IDS.weekly,
61
- priceLabel: '$7.99/week',
62
- perDayAmount: '$1.14',
63
- amountCents: 799,
64
- checkoutSummaryLabel: '$7.99 today',
65
- });
66
- expect(billing.plans.secondary).toEqual({
67
- projectPlanId: CLAIMBEE_PROJECT_PLAN_IDS.monthly,
68
- title: '1-Month Plan',
69
- providerPlanId: CLAIMBEE_LIVE_PROVIDER_PLAN_IDS.monthly,
70
- featuredTag: '🔥 Most Popular Choice',
71
- priceLabel: '$29.99/month',
72
- perDayAmount: '$1.00',
73
- amountCents: 2999,
74
- checkoutSummaryLabel: '$29.99 today',
75
- });
76
- expect(billingPlans).toBe(billing.plans);
77
- expect('id' in billing.plans.primary).toBe(false);
78
- expect('oldPriceLabel' in billing.plans.primary).toBe(false);
79
- expect('perDayLabel' in billing.plans.primary).toBe(false);
80
- expect('billingInterval' in billing.plans.primary).toBe(false);
81
- expect('isDefault' in billing.plans.secondary).toBe(false);
82
- expect(billing.discounts.map((discount) => discount.stage)).toEqual(['first', 'second']);
83
- expect(billingDiscounts.first.couponId).toBe('fg_30_off_one');
84
- expect(billingDiscounts.second.couponId).toBe('fg_46_off_once');
85
- expect(billingDiscountList).toBe(billing.discounts);
86
- });
87
-
88
- it('uses test provider price ids when Stripe mode is test', async () => {
89
- stubRuntimeMode('test');
90
- const { billing } = await import('../../../src/config/billing.plans');
91
-
92
- expect(billing.plans.primary.projectPlanId).toBe(CLAIMBEE_PROJECT_PLAN_IDS.weekly);
93
- expect(billing.plans.primary.providerPlanId).toBe(CLAIMBEE_TEST_PROVIDER_PLAN_IDS.weekly);
94
- expect(billing.plans.secondary.projectPlanId).toBe(CLAIMBEE_PROJECT_PLAN_IDS.monthly);
95
- expect(billing.plans.secondary.providerPlanId).toBe(CLAIMBEE_TEST_PROVIDER_PLAN_IDS.monthly);
96
- expect(billing.plans.tertiary.projectPlanId).toBe(CLAIMBEE_PROJECT_PLAN_IDS.yearly);
97
- expect(billing.plans.tertiary.providerPlanId).toBe(CLAIMBEE_TEST_PROVIDER_PLAN_IDS.yearly);
98
- });
99
-
100
- it('exports the split test plan catalog under the generic billingTestPlans name', async () => {
101
- const billingTestModule = await import('../../../src/config/billing.test.plans') as Record<string, unknown>;
102
-
103
- expect(billingTestModule.billingTestPlans).toEqual({
104
- primary: expect.objectContaining({
105
- projectPlanId: CLAIMBEE_PROJECT_PLAN_IDS.weekly,
106
- providerPlanId: CLAIMBEE_TEST_PROVIDER_PLAN_IDS.weekly,
107
- }),
108
- secondary: expect.objectContaining({
109
- projectPlanId: CLAIMBEE_PROJECT_PLAN_IDS.monthly,
110
- providerPlanId: CLAIMBEE_TEST_PROVIDER_PLAN_IDS.monthly,
111
- }),
112
- tertiary: expect.objectContaining({
113
- projectPlanId: CLAIMBEE_PROJECT_PLAN_IDS.yearly,
114
- providerPlanId: CLAIMBEE_TEST_PROVIDER_PLAN_IDS.yearly,
115
- }),
116
- });
117
- });
118
-
119
- it('lets payments helpers derive ordered paywall plans from semantic keys', async () => {
120
- const { billingPlans } = await import('../../../src/config/billing.plans');
121
-
122
- expect(buildConfigPaywallPlans(billingPlans, ['primary', 'secondary', 'tertiary'])).toEqual([
123
- expect.objectContaining({
124
- id: 'primary',
125
- providerPlanId: CLAIMBEE_LIVE_PROVIDER_PLAN_IDS.weekly,
126
- perDayLabel: 'per day',
127
- billingInterval: 'week',
128
- source: 'config',
129
- }),
130
- expect.objectContaining({
131
- id: 'secondary',
132
- providerPlanId: CLAIMBEE_LIVE_PROVIDER_PLAN_IDS.monthly,
133
- featuredTag: '🔥 Most Popular Choice',
134
- billingInterval: 'month',
135
- }),
136
- expect.objectContaining({
137
- id: 'tertiary',
138
- providerPlanId: CLAIMBEE_LIVE_PROVIDER_PLAN_IDS.yearly,
139
- billingInterval: 'year',
140
- }),
141
- ]);
142
- });
143
- });
@@ -1,166 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- CURRENT_STEP_CONTRACT_HASH,
4
- CURRENT_STEP_CONTRACT_VERSION,
5
- validateFunnelStepContractSnapshot,
6
- type FunnelStepContractProjectSnapshot,
7
- } from '@funnelsgrove/runtime';
8
- import { stepRegistry } from '@/runtime/step-registry';
9
- import { stepTwoContent, stepTwoOptions } from '@/steps/content/step-02.content';
10
- import { contentOptionsByStepId } from '@/steps/step-content.registry';
11
- import { funnelManifest } from '../../../src/config/funnel.manifest';
12
-
13
- describe('funnel manifest breakpoints', () => {
14
- it('ships the four default QA breakpoints for new funnels', () => {
15
- expect(funnelManifest.breakpoints).toEqual({
16
- small: { width: 375, height: 667 },
17
- medium: { width: 393, height: 852 },
18
- large: { width: 402, height: 874 },
19
- 'desktop-small': { width: 1280, height: 800 },
20
- });
21
- });
22
- });
23
-
24
- describe('funnel step contract metadata', () => {
25
- it('authors the canonical version 3 reserved identities', () => {
26
- expect(funnelManifest.stepContractVersion).toBe(3);
27
- expect(
28
- funnelManifest.steps.map(({ id, name, type, kind, choice }) => ({
29
- id,
30
- name,
31
- type,
32
- kind,
33
- choice,
34
- })),
35
- ).toEqual([
36
- { id: 'step-1', name: 'step-1', type: 'intro_hero', kind: undefined, choice: undefined },
37
- {
38
- id: 'step-2',
39
- name: 'step-2',
40
- type: 'multi_select_choice',
41
- kind: undefined,
42
- choice: { answerKey: 'selectedDevices', allowEmpty: true },
43
- },
44
- {
45
- id: 'step-3',
46
- name: 'step-3',
47
- type: 'progress_interstitial',
48
- kind: undefined,
49
- choice: undefined,
50
- },
51
- {
52
- id: 'step-4',
53
- name: 'scratchpad-entry',
54
- type: 'progress_interstitial',
55
- kind: undefined,
56
- choice: undefined,
57
- },
58
- {
59
- id: 'email-capture',
60
- name: 'email-capture',
61
- type: 'form_input',
62
- kind: undefined,
63
- choice: undefined,
64
- },
65
- {
66
- id: 'paywall',
67
- name: 'paywall',
68
- type: 'paywall_offer',
69
- kind: 'paywall',
70
- choice: undefined,
71
- },
72
- {
73
- id: 'paywall-b',
74
- name: 'paywall-b',
75
- type: 'paywall_offer',
76
- kind: 'paywall',
77
- choice: undefined,
78
- },
79
- {
80
- id: 'manage-subscription',
81
- name: 'manage-subscription',
82
- type: 'subscription_management',
83
- kind: 'manage-subscription',
84
- choice: undefined,
85
- },
86
- {
87
- id: 'subscription-started',
88
- name: 'subscription-started',
89
- type: 'purchase_completed',
90
- kind: 'subscription-handoff',
91
- choice: undefined,
92
- },
93
- ]);
94
- });
95
-
96
- it('classifies only plan-offer screens as paywalls', () => {
97
- expect(
98
- funnelManifest.steps
99
- .filter((step) => step.type === 'paywall_offer')
100
- .map((step) => step.id),
101
- ).toEqual(['paywall', 'paywall-b']);
102
- });
103
-
104
- it('keeps component metadata and registered keys aligned with the manifest', () => {
105
- expect(Object.keys(stepRegistry)).toEqual(funnelManifest.steps.map((step) => step.componentKey));
106
-
107
- for (const step of funnelManifest.steps) {
108
- const componentMeta = stepRegistry[step.componentKey].meta;
109
- expect({
110
- id: componentMeta.id,
111
- name: componentMeta.name,
112
- type: componentMeta.type,
113
- kind: componentMeta.kind,
114
- choice: componentMeta.choice,
115
- }).toEqual({
116
- id: step.id,
117
- name: step.name,
118
- type: step.type,
119
- kind: step.kind,
120
- choice: step.choice,
121
- });
122
- }
123
- });
124
-
125
- it('keeps Step Two option IDs immutable', () => {
126
- expect(contentOptionsByStepId['step-2']).toBe(stepTwoOptions);
127
- expect(Object.isFrozen(stepTwoOptions)).toBe(true);
128
- expect(stepTwoOptions.every((option) => Object.isFrozen(option))).toBe(true);
129
- expect(stepTwoOptions).toEqual([
130
- { id: 'iphone', label: 'Smartphone', imageSrc: '/claimbee/device-iphone.png' },
131
- { id: 'laptop', label: 'Laptop', imageSrc: '/claimbee/device-macbook.png' },
132
- { id: 'watch', label: 'Smart watch', imageSrc: '/claimbee/device-watch.png' },
133
- { id: 'none', label: 'None of these', imageSrc: '/claimbee/device-none.png' },
134
- ]);
135
- expect(stepTwoContent.locales.en.options.map((option) => option.id)).toEqual([
136
- 'iphone',
137
- 'laptop',
138
- 'watch',
139
- 'none',
140
- ]);
141
- });
142
-
143
- it('passes the complete authoring snapshot contract without diagnostics', () => {
144
- const snapshot = {
145
- manifest: funnelManifest,
146
- componentMetaByKey: Object.fromEntries(
147
- Object.entries(stepRegistry).map(([componentKey, entry]) => [componentKey, entry.meta]),
148
- ),
149
- registeredComponentKeys: Object.keys(stepRegistry),
150
- contentOptionsByStepId,
151
- edgesByStepId: funnelManifest.edgesByStepId,
152
- experiments: funnelManifest.experiments,
153
- docs: {
154
- schemaVersion: 1,
155
- bundleVersion: '2.0.0',
156
- stepContractVersion: CURRENT_STEP_CONTRACT_VERSION,
157
- contractHash: CURRENT_STEP_CONTRACT_HASH,
158
- missing: [],
159
- changed: [],
160
- conflicts: [],
161
- },
162
- } satisfies FunnelStepContractProjectSnapshot;
163
-
164
- expect(validateFunnelStepContractSnapshot(snapshot, { mode: 'authoring' })).toEqual([]);
165
- });
166
- });
@@ -1,79 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- createFunnelContractDiagnostic,
4
- CURRENT_STEP_CONTRACT_HASH,
5
- CURRENT_STEP_CONTRACT_VERSION,
6
- type FunnelStepContractProjectSnapshot,
7
- } from '@funnelsgrove/runtime';
8
- import docsManifest from '../../../.funnelsgrove-docs.json';
9
- import { funnelManifest, rawFunnelManifest } from '@/config/funnel.manifest';
10
- import { stepRegistry } from '@/runtime/step-registry';
11
- import { contentOptionsByStepId } from '@/steps/step-content.registry';
12
-
13
- const loadProjectValidation = async () => {
14
- return import('../../../src/contract/funnel-project.validation');
15
- };
16
-
17
- describe('funnel project validation', () => {
18
- it('builds the complete project snapshot from the authored registries', async () => {
19
- const projectValidation = await loadProjectValidation();
20
-
21
- expect(projectValidation).not.toBeNull();
22
- const snapshot = projectValidation.buildFunnelProjectSnapshot();
23
-
24
- expect(snapshot).toEqual({
25
- manifest: rawFunnelManifest,
26
- componentMetaByKey: Object.fromEntries(
27
- Object.entries(stepRegistry).map(([componentKey, entry]) => [componentKey, entry.meta]),
28
- ),
29
- registeredComponentKeys: Object.keys(stepRegistry),
30
- contentOptionsByStepId,
31
- edgesByStepId: rawFunnelManifest.edgesByStepId,
32
- experiments: rawFunnelManifest.experiments,
33
- docs: {
34
- schemaVersion: docsManifest.schemaVersion,
35
- bundleVersion: docsManifest.bundleVersion,
36
- stepContractVersion: docsManifest.stepContractVersion,
37
- contractHash: docsManifest.contractHash,
38
- missing: [],
39
- changed: [],
40
- conflicts: [],
41
- },
42
- });
43
- expect(projectValidation.FUNNEL_PROJECT_CONTRACT_VERSION)
44
- .toBe(CURRENT_STEP_CONTRACT_VERSION);
45
- expect(funnelManifest).toBe(rawFunnelManifest);
46
- });
47
-
48
- it('returns pure authoring diagnostics without changing their fields', async () => {
49
- const { buildFunnelProjectSnapshot, validateFunnelProject } = await loadProjectValidation();
50
- const snapshot = structuredClone(
51
- buildFunnelProjectSnapshot(),
52
- ) as FunnelStepContractProjectSnapshot;
53
- const paywall = snapshot.componentMetaByKey.stepPaywall;
54
- snapshot.componentMetaByKey.stepPaywall = {
55
- ...paywall,
56
- name: 'renamed-paywall',
57
- };
58
- const expectedDiagnostic = createFunnelContractDiagnostic({
59
- code: 'FG-PARITY-001',
60
- file: 'src/steps/step-32-paywall.tsx',
61
- stepId: 'paywall',
62
- expected: 'paywall',
63
- received: 'renamed-paywall',
64
- reason: 'Component metadata name does not match the manifest step.',
65
- guide: 'docs/product-specs/funnel-template-step-authoring.md',
66
- repair: 'Update the component metadata name to match the manifest step.',
67
- });
68
-
69
- expect(validateFunnelProject(snapshot)).toEqual([expectedDiagnostic]);
70
- expect(snapshot.componentMetaByKey.stepPaywall.name).toBe('renamed-paywall');
71
- });
72
-
73
- it('validates the checked-in template without diagnostics', async () => {
74
- const { validateFunnelProject } = await loadProjectValidation();
75
-
76
- expect(validateFunnelProject()).toEqual([]);
77
- expect(CURRENT_STEP_CONTRACT_HASH).toBe(docsManifest.contractHash);
78
- });
79
- });