@funnelsgrove/cli 0.1.29 → 0.1.43
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/cli.d.ts +44 -0
- package/dist/cli.js +74 -24
- package/funnel-contract-compatibility.json +56 -56
- package/package.json +4 -3
- package/template_docs/.funnelsgrove-docs.json +7 -7
- package/template_docs/docs/funnelsgrove/START-HERE.md +4 -1
- package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +4 -2
- package/template_docs/docs/funnelsgrove/contracts/payments.md +3 -1
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_docs/docs/funnelsgrove/qa/analytics.md +3 -1
- package/template_docs/funnel-docs.config.json +1 -1
- package/template_scaffold/.funnelsgrove-docs.json +7 -7
- package/template_scaffold/.funnelsgrove-scaffold.json +159 -154
- package/template_scaffold/docs/funnelsgrove/START-HERE.md +4 -1
- package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +4 -2
- package/template_scaffold/docs/funnelsgrove/contracts/payments.md +3 -1
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_scaffold/docs/funnelsgrove/qa/analytics.md +3 -1
- package/template_scaffold/funnel-docs.config.json +1 -1
- package/template_scaffold/package-lock.json +13 -13
- package/template_scaffold/package.json +3 -3
- package/template_scaffold/public/paywall/money-back-badge.png +0 -0
- package/template_scaffold/src/steps/step-32-paywall.tsx +2 -2
- package/template_scaffold/src/steps/step-33-subscription-started.tsx +1 -43
- package/template_scaffold/{funnel-agent-docs.test.ts → tests/funnel-agent-docs.test.ts} +29 -19
- package/template_scaffold/{package.test.ts → tests/package.test.ts} +4 -4
- package/template_scaffold/{src → tests/src}/app/layout.test.ts +2 -2
- package/template_scaffold/{src → tests/src}/components/FunnelFlow.test.tsx +2 -2
- package/template_scaffold/{src → tests/src}/config/billing.plans.test.ts +4 -4
- package/template_scaffold/{src → tests/src}/config/experiments.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/config/funnel.manifest.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/contract/funnel-project.validation.test.ts +2 -2
- package/template_scaffold/{src → tests/src}/contract/golden/golden-fixtures.test.ts +16 -16
- package/template_scaffold/{src → tests/src}/contract/validate-funnel.cli.test.ts +2 -2
- package/template_scaffold/{src → tests/src}/runtime/checkout-runtime-config.test.ts +3 -3
- package/template_scaffold/{src → tests/src}/runtime/funnel-runtime.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/runtime/step-content-context.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/runtime/use-funnel-flow-controller.integration.test.ts +4 -4
- package/template_scaffold/{src → tests/src}/runtime/use-funnel-flow-controller.test.ts +3 -3
- package/template_scaffold/{src → tests/src}/steps/paywall-checkout-options.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/steps/shared-header-contract.test.ts +3 -1
- package/template_scaffold/{src → tests/src}/steps/step-02.test.ts +3 -3
- package/template_scaffold/{src → tests/src}/steps/step-03.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/steps/step-04.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/steps/step-31-email-capture.test.ts +1 -1
- package/template_scaffold/{src → tests/src}/steps/step-32-paywall.test.ts +43 -43
- package/template_scaffold/{src → tests/src}/steps/step-33-subscription-started.test.ts +6 -7
- package/template_scaffold/{src → tests/src}/steps/step-34-upsell-form.test.ts +3 -3
- package/template_scaffold/{src → tests/src}/steps/step-image-migration.test.ts +1 -1
- package/template_scaffold/vercel.json +6 -0
- /package/template_scaffold/{src → tests/src}/app/[stepId]/page.test.tsx +0 -0
- /package/template_scaffold/{src → tests/src}/components/FunnelStepImage.test.ts +0 -0
- /package/template_scaffold/{src → tests/src}/steps/step-01.test.ts +0 -0
|
@@ -54,7 +54,10 @@ Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf
|
|
|
54
54
|
- A paywall presents purchasable plans. A loading, reveal, or “ready” screen is an interstitial.
|
|
55
55
|
- Every choice declares `choice.answerKey`, stores stable option IDs in the exact scalar/array shape, and uses `useStepChoices`.
|
|
56
56
|
- Email capture uses `submitEmailCapture`; it never manually sends analytics or hard-codes paywall navigation.
|
|
57
|
-
-
|
|
57
|
+
- Quiz completed counts each unique user who reaches a paywall step.
|
|
58
|
+
- `purchase_completed` remains trusted-server-only purchase verification and does not define Quiz completed.
|
|
59
|
+
- Browser terminal reach and `funnel_completed` remain lifecycle-only and are not purchase conversions.
|
|
60
|
+
- `subscription-started` remains the contract-v3 `purchase_completed` terminal type, and only the trusted backend records the purchase event after verifying an initial purchase.
|
|
58
61
|
- Let the controller distinguish completion from exit and suppress provider delivery in preview.
|
|
59
62
|
|
|
60
63
|
## Standard workflow
|
|
@@ -24,7 +24,9 @@ Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf
|
|
|
24
24
|
| `step_exited` | `step_exited` | `step_exit` | `any` | `any` | `controller` | `true` | `step-visit` | `funnelId`, `funnelVersionId`, `environment`, `stepContractVersion`, `stepId`, `stepName`, `stepType` |
|
|
25
25
|
| `step_started` | `step_started` | `step_start` | `any` | `any` | `controller` | `true` | `step-visit` | `funnelId`, `funnelVersionId`, `environment`, `stepContractVersion`, `stepId`, `stepName`, `stepType` |
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Quiz completed counts each unique user who reaches a paywall step. `purchase_completed` remains trusted-server-only purchase verification and does not define Quiz completed. Browser terminal reach and `funnel_completed` remain lifecycle-only and are not purchase conversions.
|
|
28
|
+
|
|
29
|
+
The `purchase_completed` event is owned by `trusted-server` and is recorded only after the backend verifies a successful initial purchase. The separate `registration_completed` event remains owned by `trusted-server` for the later app-link claim. The `email_captured` event is owned by `transactional-server` and uses `logical-email-conversion` deduplication.
|
|
28
30
|
<!-- funnelsgrove:generated:end contract-v3/contract/analytics-events -->
|
|
29
31
|
|
|
30
32
|
## Ownership rules
|
|
@@ -34,7 +36,7 @@ The `purchase_completed` event is owned by `trusted-server` and is recorded only
|
|
|
34
36
|
- `submitEmailCapture` creates `email_captured` transactionally, then routes only after success.
|
|
35
37
|
- Named payment helpers emit checkout semantic events at real payment boundaries.
|
|
36
38
|
- The terminal helper owns funnel completion.
|
|
37
|
-
- A trusted backend owns `purchase_completed` after verifying an initial subscription or one-time payment; browser code never emits it
|
|
39
|
+
- A trusted backend owns `purchase_completed` after verifying an initial subscription or one-time payment; browser code never emits it. This purchase event is independent from the paywall-reach definition of Quiz completed.
|
|
38
40
|
- A separate trusted server claim owns `registration_completed` when the purchased subscription is later linked in the app.
|
|
39
41
|
- Provider adapters and the durable outbox own PostHog and server Meta delivery.
|
|
40
42
|
|
|
@@ -37,6 +37,8 @@ A paywall presents purchasable plans. A reveal, loading state, calculation, or
|
|
|
37
37
|
|
|
38
38
|
## Purchase completion separation
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Quiz completed counts each unique user who reaches a paywall step. `purchase_completed` remains trusted-server-only purchase verification and does not define Quiz completed. Browser terminal reach and `funnel_completed` remain lifecycle-only and are not purchase conversions.
|
|
41
|
+
|
|
42
|
+
The canonical `subscription-started` route keeps its stable identity and remains the `purchase_completed` terminal type in contract v3. Only the trusted backend emits the purchase event after verifying an initial subscription or one-time payment; renewals, pending payments, and browser checkout returns do not qualify. `registration_completed` remains the later app-link conversion.
|
|
41
43
|
|
|
42
44
|
Before publishing, complete [paywall/checkout QA](../qa/paywall-checkout.md) and [publish QA](../qa/publish.md).
|
|
@@ -17,7 +17,7 @@ Supported read versions: `1`, `2`, `3`. Authoring and publish target version `3`
|
|
|
17
17
|
|
|
18
18
|
### Package release order
|
|
19
19
|
|
|
20
|
-
Release `@funnelsgrove/runtime` `0.1.
|
|
20
|
+
Release `@funnelsgrove/runtime` `0.1.63` first, then `@funnelsgrove/analytics` `0.1.41`, then `@funnelsgrove/payments` `0.1.58`. Deploy the API and funnel template, then confirm production `/health` reports the new docs identity. Only then publish `@funnelsgrove/cli` `0.1.43`. Publishing packages and deploying production remain separately approved operational actions.
|
|
21
21
|
<!-- funnelsgrove:generated:end contract-v3/migration/step-contract-v3 -->
|
|
22
22
|
|
|
23
23
|
## Version-last policy
|
|
@@ -32,7 +32,9 @@ Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf
|
|
|
32
32
|
- Choice steps save the exact stable ID shape before completion.
|
|
33
33
|
- Email success creates one server conversion; duplicate capture skips browser fanout; failure changes no state/lifecycle/route.
|
|
34
34
|
- Checkout helpers fire only at real boundaries with complete metadata and stable event IDs.
|
|
35
|
-
- Quiz completed
|
|
35
|
+
- Quiz completed counts each unique user who reaches a paywall step.
|
|
36
|
+
- `purchase_completed` remains trusted-server-only purchase verification and does not define Quiz completed.
|
|
37
|
+
- Browser terminal reach and `funnel_completed` remain lifecycle-only and are not purchase conversions.
|
|
36
38
|
- `registration_completed` remains the separate later app-link claim.
|
|
37
39
|
- Preview sends no API/provider analytics.
|
|
38
40
|
- No component sends PostHog or server Meta directly.
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"name": "funnel-template",
|
|
9
9
|
"version": "0.1.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@funnelsgrove/analytics": "^0.1.
|
|
12
|
-
"@funnelsgrove/payments": "^0.1.
|
|
13
|
-
"@funnelsgrove/runtime": "^0.1.
|
|
11
|
+
"@funnelsgrove/analytics": "^0.1.41",
|
|
12
|
+
"@funnelsgrove/payments": "^0.1.58",
|
|
13
|
+
"@funnelsgrove/runtime": "^0.1.63",
|
|
14
14
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
15
15
|
"@stripe/stripe-js": "^8.7.0",
|
|
16
16
|
"lucide-react": "^0.553.0",
|
|
@@ -891,17 +891,17 @@
|
|
|
891
891
|
}
|
|
892
892
|
},
|
|
893
893
|
"node_modules/@funnelsgrove/analytics": {
|
|
894
|
-
"version": "0.1.
|
|
895
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/analytics/-/analytics-0.1.
|
|
896
|
-
"integrity": "sha512-
|
|
894
|
+
"version": "0.1.41",
|
|
895
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/analytics/-/analytics-0.1.41.tgz",
|
|
896
|
+
"integrity": "sha512-RrAlBajsJawxpG1ijthRhGcuyqoPCP29r4XNu3K980zrtkWidYu0E4TqBPdPHTmhBb6WNnMcfVHV6H7XaqkQ8A==",
|
|
897
897
|
"dependencies": {
|
|
898
|
-
"@funnelsgrove/runtime": "0.1.
|
|
898
|
+
"@funnelsgrove/runtime": "0.1.63"
|
|
899
899
|
}
|
|
900
900
|
},
|
|
901
901
|
"node_modules/@funnelsgrove/payments": {
|
|
902
|
-
"version": "0.1.
|
|
903
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/payments/-/payments-0.1.
|
|
904
|
-
"integrity": "sha512-
|
|
902
|
+
"version": "0.1.58",
|
|
903
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/payments/-/payments-0.1.58.tgz",
|
|
904
|
+
"integrity": "sha512-y1caUil8SFNiuYf/TxptheEf/cnK2urafy2eF2gFdfugfaybFiqUiyTzEVen+K+uKtX8tDCk2UtNeyWMO8inAA==",
|
|
905
905
|
"dependencies": {
|
|
906
906
|
"@funnelsgrove/analytics": "^0.1.28",
|
|
907
907
|
"@funnelsgrove/runtime": "^0.1.54",
|
|
@@ -913,9 +913,9 @@
|
|
|
913
913
|
}
|
|
914
914
|
},
|
|
915
915
|
"node_modules/@funnelsgrove/runtime": {
|
|
916
|
-
"version": "0.1.
|
|
917
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/runtime/-/runtime-0.1.
|
|
918
|
-
"integrity": "sha512-
|
|
916
|
+
"version": "0.1.63",
|
|
917
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/runtime/-/runtime-0.1.63.tgz",
|
|
918
|
+
"integrity": "sha512-D0y7l/GwVc519CBr3TCxsXvr3H+B9RmcvebBUKks+FqdDx5j2AO5qdUbB838bqkBNCCjHOtcbs3u2Gsm4Qhk4w==",
|
|
919
919
|
"dependencies": {
|
|
920
920
|
"posthog-js": "^1.369.2",
|
|
921
921
|
"react": "19.2.3",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"validate:funnel": "vite-node --config src/contract/funnel-validator.vite.config.ts src/contract/validate-funnel.cli.ts"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@funnelsgrove/analytics": "^0.1.
|
|
16
|
-
"@funnelsgrove/payments": "^0.1.
|
|
17
|
-
"@funnelsgrove/runtime": "^0.1.
|
|
15
|
+
"@funnelsgrove/analytics": "^0.1.41",
|
|
16
|
+
"@funnelsgrove/payments": "^0.1.58",
|
|
17
|
+
"@funnelsgrove/runtime": "^0.1.63",
|
|
18
18
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
19
19
|
"@stripe/stripe-js": "^8.7.0",
|
|
20
20
|
"lucide-react": "^0.553.0",
|
|
Binary file
|
|
@@ -463,7 +463,7 @@ export function StepPaywall({
|
|
|
463
463
|
return;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
const readyClientSecret = await checkoutSession.prepareCardCheckout();
|
|
466
|
+
const readyClientSecret = await checkoutSession.prepareCardCheckout({ checkoutStartSource: 'checkout_render' });
|
|
467
467
|
if (readyClientSecret) {
|
|
468
468
|
setCheckoutDialogOpen(true);
|
|
469
469
|
}
|
|
@@ -510,7 +510,7 @@ export function StepPaywall({
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
const readyClientSecret =
|
|
513
|
-
checkoutSession.activeClientSecret ?? await checkoutSession.prepareCardCheckout();
|
|
513
|
+
checkoutSession.activeClientSecret ?? await checkoutSession.prepareCardCheckout({ checkoutStartSource: 'checkout_render' });
|
|
514
514
|
if (readyClientSecret) {
|
|
515
515
|
setCheckoutDialogOpen(true);
|
|
516
516
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// PLAN_STEP_PLACEHOLDER_ID:subscription-started
|
|
2
2
|
'use client';
|
|
3
3
|
|
|
4
|
-
import { useEffect } from 'react';
|
|
5
|
-
import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
|
|
6
4
|
import {
|
|
7
5
|
SubscriptionHandoffScreen,
|
|
8
6
|
usePreviewStepLocalizedContent,
|
|
@@ -11,7 +9,6 @@ import {
|
|
|
11
9
|
} from '@funnelsgrove/runtime';
|
|
12
10
|
import { getStepContentLocale } from '@/runtime/step-content-context';
|
|
13
11
|
import { subscriptionStartedContent } from '@/steps/content/subscription-started.content';
|
|
14
|
-
import { funnelManifest } from '@/config/funnel.manifest';
|
|
15
12
|
|
|
16
13
|
export const stepSubscriptionStartedId = 'subscription-started';
|
|
17
14
|
|
|
@@ -29,52 +26,13 @@ export const stepSubscriptionStarted: FunnelStepMeta = {
|
|
|
29
26
|
};
|
|
30
27
|
|
|
31
28
|
export function StepSubscriptionStarted() {
|
|
32
|
-
const {
|
|
29
|
+
const { attributes } = useFunnel();
|
|
33
30
|
const content = usePreviewStepLocalizedContent(
|
|
34
31
|
stepSubscriptionStartedId,
|
|
35
32
|
subscriptionStartedContent,
|
|
36
33
|
getStepContentLocale(attributes),
|
|
37
34
|
);
|
|
38
35
|
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (typeof window === 'undefined') {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const searchParams = new URLSearchParams(window.location.search);
|
|
45
|
-
const paymentIntent = searchParams.get('payment_intent');
|
|
46
|
-
const redirectStatus = searchParams.get('redirect_status');
|
|
47
|
-
if (!paymentIntent || !redirectStatus) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const dedupeKey = `app-deals:payment-intent:${paymentIntent}`;
|
|
52
|
-
if (window.sessionStorage.getItem(dedupeKey) === '1') {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
window.sessionStorage.setItem(dedupeKey, '1');
|
|
56
|
-
|
|
57
|
-
const eventInput = {
|
|
58
|
-
stepId: stepSubscriptionStartedId,
|
|
59
|
-
stepName: stepSubscriptionStarted.name || stepSubscriptionStarted.title,
|
|
60
|
-
stepType: stepSubscriptionStarted.type,
|
|
61
|
-
stepContractVersion: funnelManifest.stepContractVersion,
|
|
62
|
-
metadata: {
|
|
63
|
-
source: searchParams.get('source') || null,
|
|
64
|
-
paymentIntent,
|
|
65
|
-
redirectStatus,
|
|
66
|
-
user_id: searchParams.get('user_id') || null,
|
|
67
|
-
funnelUserId: user.id || null,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (redirectStatus === 'succeeded') {
|
|
72
|
-
publicAnalyticsSdk.trackPaymentCheckoutSucceeded(eventInput);
|
|
73
|
-
} else {
|
|
74
|
-
publicAnalyticsSdk.trackPaymentCheckoutReturned(eventInput);
|
|
75
|
-
}
|
|
76
|
-
}, [user.id]);
|
|
77
|
-
|
|
78
36
|
return (
|
|
79
37
|
<SubscriptionHandoffScreen
|
|
80
38
|
stepId={stepSubscriptionStartedId}
|
|
@@ -7,9 +7,9 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { describe, expect, it } from 'vitest';
|
|
9
9
|
|
|
10
|
-
import contract from '
|
|
10
|
+
import contract from '../../funnel-runtime/contracts/step-contract-v2.json';
|
|
11
11
|
|
|
12
|
-
const templateRoot = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const templateRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
13
13
|
const repoRoot = resolve(templateRoot, '../..');
|
|
14
14
|
const cliDocsRoot = join(repoRoot, 'apps/cli/template_docs');
|
|
15
15
|
|
|
@@ -34,7 +34,7 @@ const renderCodes = (values: readonly unknown[]): string => (
|
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
async function loadGenerator() {
|
|
37
|
-
return import('
|
|
37
|
+
return import('../../../scripts/generate-funnel-agent-docs.mjs');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
describe('funnel agent documentation supply', () => {
|
|
@@ -300,21 +300,31 @@ describe('funnel agent documentation supply', () => {
|
|
|
300
300
|
}
|
|
301
301
|
});
|
|
302
302
|
|
|
303
|
-
it('
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
'
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
303
|
+
it('defines Quiz completed by unique paywall reach without conflating purchase verification', async () => {
|
|
304
|
+
const paths = [
|
|
305
|
+
'docs/funnelsgrove/qa/analytics.md',
|
|
306
|
+
'docs/funnelsgrove/contracts/analytics-events.md',
|
|
307
|
+
'docs/funnelsgrove/START-HERE.md',
|
|
308
|
+
'docs/funnelsgrove/contracts/payments.md',
|
|
309
|
+
];
|
|
310
|
+
const contents = await Promise.all(paths.map(async (path) => ({
|
|
311
|
+
path,
|
|
312
|
+
content: await readFile(join(templateRoot, path), 'utf8'),
|
|
313
|
+
})));
|
|
314
|
+
|
|
315
|
+
for (const { path, content } of contents) {
|
|
316
|
+
expect(content, path).toContain(
|
|
317
|
+
'Quiz completed counts each unique user who reaches a paywall step.',
|
|
318
|
+
);
|
|
319
|
+
expect(content, path).toContain(
|
|
320
|
+
'`purchase_completed` remains trusted-server-only purchase verification and does not define Quiz completed.',
|
|
321
|
+
);
|
|
322
|
+
expect(content, path).toContain(
|
|
323
|
+
'Browser terminal reach and `funnel_completed` remain lifecycle-only and are not purchase conversions.',
|
|
324
|
+
);
|
|
325
|
+
expect(content, path).not.toMatch(/Quiz completed requires[^.]*purchase_completed/i);
|
|
326
|
+
expect(content, path).not.toMatch(/terminal reach[^.]*not (?:count|counts) as Quiz completed/i);
|
|
327
|
+
}
|
|
318
328
|
});
|
|
319
329
|
|
|
320
330
|
it('preserves the implementation-owned visitor metric while linking event ownership', async () => {
|
|
@@ -340,7 +350,7 @@ describe('funnel agent documentation supply', () => {
|
|
|
340
350
|
|
|
341
351
|
expect(manifest).toMatchObject({
|
|
342
352
|
schemaVersion: 1,
|
|
343
|
-
bundleVersion: '2.0.
|
|
353
|
+
bundleVersion: '2.0.31',
|
|
344
354
|
stepContractVersion: contract.stepContractVersion,
|
|
345
355
|
contractHash: contract.contractHash,
|
|
346
356
|
});
|
|
@@ -11,10 +11,10 @@ const readPackageJson = (url: URL): PackageJson =>
|
|
|
11
11
|
|
|
12
12
|
describe('funnel-template shared package dependencies', () => {
|
|
13
13
|
it('tracks the current shared runtime, analytics, and payments package versions', () => {
|
|
14
|
-
const templatePackage = readPackageJson(new URL('
|
|
15
|
-
const runtimePackage = readPackageJson(new URL('
|
|
16
|
-
const analyticsPackage = readPackageJson(new URL('
|
|
17
|
-
const paymentsPackage = readPackageJson(new URL('
|
|
14
|
+
const templatePackage = readPackageJson(new URL('../package.json', import.meta.url));
|
|
15
|
+
const runtimePackage = readPackageJson(new URL('../../funnel-runtime/package.json', import.meta.url));
|
|
16
|
+
const analyticsPackage = readPackageJson(new URL('../../funnel-analytics/package.json', import.meta.url));
|
|
17
|
+
const paymentsPackage = readPackageJson(new URL('../../funnel-payments/package.json', import.meta.url));
|
|
18
18
|
|
|
19
19
|
expect(templatePackage.dependencies?.['@funnelsgrove/runtime']).toBe(`^${runtimePackage.version}`);
|
|
20
20
|
expect(templatePackage.dependencies?.['@funnelsgrove/analytics']).toBe(`^${analyticsPackage.version}`);
|
|
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
4
4
|
|
|
5
5
|
describe('RootLayout metadata source', () => {
|
|
6
6
|
it('wires project page metadata settings into Next metadata', () => {
|
|
7
|
-
const layoutSource = readFileSync(path.resolve(__dirname, '
|
|
7
|
+
const layoutSource = readFileSync(path.resolve(__dirname, '../../../src/app/layout.tsx'), 'utf8');
|
|
8
8
|
|
|
9
9
|
expect(layoutSource).toContain('runtimePublicConfig.pageTitle');
|
|
10
10
|
expect(layoutSource).toContain('runtimePublicConfig.pageDescription');
|
|
@@ -15,7 +15,7 @@ describe('RootLayout metadata source', () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('renders Google tag head scripts through the shared analytics package', () => {
|
|
18
|
-
const layoutSource = readFileSync(path.resolve(__dirname, '
|
|
18
|
+
const layoutSource = readFileSync(path.resolve(__dirname, '../../../src/app/layout.tsx'), 'utf8');
|
|
19
19
|
|
|
20
20
|
expect(layoutSource).toContain("import { buildGoogleTagHeadScripts } from '@funnelsgrove/analytics'");
|
|
21
21
|
expect(layoutSource).toContain('const googleTagHeadScripts = buildGoogleTagHeadScripts();');
|
|
@@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
describe('FunnelFlow lifecycle wiring', () => {
|
|
5
5
|
it('delegates one analytics adapter and the authoritative manifest version to the shared controller', () => {
|
|
6
|
-
const flowSource = readFileSync(new URL('
|
|
6
|
+
const flowSource = readFileSync(new URL('../../../src/components/FunnelFlow.tsx', import.meta.url), 'utf8');
|
|
7
7
|
const adapterSource = readFileSync(
|
|
8
|
-
new URL('
|
|
8
|
+
new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url),
|
|
9
9
|
'utf8',
|
|
10
10
|
);
|
|
11
11
|
|
|
@@ -45,7 +45,7 @@ describe('claimbee billing contract', () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it('exports minimal live billing config and normalized discounts', async () => {
|
|
48
|
-
const billingModule = await import('
|
|
48
|
+
const billingModule = await import('../../../src/config/billing.plans');
|
|
49
49
|
const {
|
|
50
50
|
billing,
|
|
51
51
|
billingDiscountList,
|
|
@@ -87,7 +87,7 @@ describe('claimbee billing contract', () => {
|
|
|
87
87
|
|
|
88
88
|
it('uses test provider price ids when Stripe mode is test', async () => {
|
|
89
89
|
stubRuntimeMode('test');
|
|
90
|
-
const { billing } = await import('
|
|
90
|
+
const { billing } = await import('../../../src/config/billing.plans');
|
|
91
91
|
|
|
92
92
|
expect(billing.plans.primary.projectPlanId).toBe(CLAIMBEE_PROJECT_PLAN_IDS.weekly);
|
|
93
93
|
expect(billing.plans.primary.providerPlanId).toBe(CLAIMBEE_TEST_PROVIDER_PLAN_IDS.weekly);
|
|
@@ -98,7 +98,7 @@ describe('claimbee billing contract', () => {
|
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
it('exports the split test plan catalog under the generic billingTestPlans name', async () => {
|
|
101
|
-
const billingTestModule = await import('
|
|
101
|
+
const billingTestModule = await import('../../../src/config/billing.test.plans') as Record<string, unknown>;
|
|
102
102
|
|
|
103
103
|
expect(billingTestModule.billingTestPlans).toEqual({
|
|
104
104
|
primary: expect.objectContaining({
|
|
@@ -117,7 +117,7 @@ describe('claimbee billing contract', () => {
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it('lets payments helpers derive ordered paywall plans from semantic keys', async () => {
|
|
120
|
-
const { billingPlans } = await import('
|
|
120
|
+
const { billingPlans } = await import('../../../src/config/billing.plans');
|
|
121
121
|
|
|
122
122
|
expect(buildConfigPaywallPlans(billingPlans, ['primary', 'secondary', 'tertiary'])).toEqual([
|
|
123
123
|
expect.objectContaining({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { experiments, manifestExperiments } from '
|
|
2
|
+
import { experiments, manifestExperiments } from '../../../src/config/experiments';
|
|
3
3
|
|
|
4
4
|
describe('funnel experiments config', () => {
|
|
5
5
|
it('ships the generic template without brand-specific experiments', () => {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { stepRegistry } from '@/runtime/step-registry';
|
|
9
9
|
import { stepTwoContent, stepTwoOptions } from '@/steps/content/step-02.content';
|
|
10
10
|
import { contentOptionsByStepId } from '@/steps/step-content.registry';
|
|
11
|
-
import { funnelManifest } from '
|
|
11
|
+
import { funnelManifest } from '../../../src/config/funnel.manifest';
|
|
12
12
|
|
|
13
13
|
describe('funnel manifest breakpoints', () => {
|
|
14
14
|
it('ships the four default QA breakpoints for new funnels', () => {
|
|
@@ -5,14 +5,14 @@ import {
|
|
|
5
5
|
CURRENT_STEP_CONTRACT_VERSION,
|
|
6
6
|
type FunnelStepContractProjectSnapshot,
|
|
7
7
|
} from '@funnelsgrove/runtime';
|
|
8
|
-
import docsManifest from '
|
|
8
|
+
import docsManifest from '../../../.funnelsgrove-docs.json';
|
|
9
9
|
import { manifestExperiments } from '@/config/experiments';
|
|
10
10
|
import { funnelManifest, rawFunnelManifest } from '@/config/funnel.manifest';
|
|
11
11
|
import { stepRegistry } from '@/runtime/step-registry';
|
|
12
12
|
import { contentOptionsByStepId } from '@/steps/step-content.registry';
|
|
13
13
|
|
|
14
14
|
const loadProjectValidation = async () => {
|
|
15
|
-
return import('
|
|
15
|
+
return import('../../../src/contract/funnel-project.validation');
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
describe('funnel project validation', () => {
|
|
@@ -9,22 +9,22 @@ import {
|
|
|
9
9
|
type FunnelManifest,
|
|
10
10
|
} from '@funnelsgrove/runtime';
|
|
11
11
|
import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
|
|
12
|
-
import { goldenFixture as cancellationOffer } from '
|
|
13
|
-
import { goldenFixture as checkout } from '
|
|
14
|
-
import { goldenFixture as purchaseCompleted } from '
|
|
15
|
-
import { goldenFixture as formInput } from '
|
|
16
|
-
import { goldenFixture as introHero } from '
|
|
17
|
-
import { goldenFixture as multiSelectChoice } from '
|
|
18
|
-
import { goldenFixture as paywallOffer } from '
|
|
19
|
-
import { goldenFixture as progressInterstitial } from '
|
|
20
|
-
import { goldenFixture as singleStepChoice } from '
|
|
21
|
-
import { goldenFixture as singleStepChoiceEmoji } from '
|
|
22
|
-
import { goldenFixture as socialProof } from '
|
|
23
|
-
import { goldenFixture as subscriptionHandoff } from '
|
|
24
|
-
import { goldenFixture as subscriptionManagement } from '
|
|
25
|
-
import { goldenFixture as summaryConfirmation } from '
|
|
26
|
-
import { goldenFixture as upsellOffer } from '
|
|
27
|
-
import { goldenFixture as valuePropStory } from '
|
|
12
|
+
import { goldenFixture as cancellationOffer } from '../../../../src/contract/golden/cancellation_offer.fixture';
|
|
13
|
+
import { goldenFixture as checkout } from '../../../../src/contract/golden/checkout.fixture';
|
|
14
|
+
import { goldenFixture as purchaseCompleted } from '../../../../src/contract/golden/purchase_completed.fixture';
|
|
15
|
+
import { goldenFixture as formInput } from '../../../../src/contract/golden/form_input.fixture';
|
|
16
|
+
import { goldenFixture as introHero } from '../../../../src/contract/golden/intro_hero.fixture';
|
|
17
|
+
import { goldenFixture as multiSelectChoice } from '../../../../src/contract/golden/multi_select_choice.fixture';
|
|
18
|
+
import { goldenFixture as paywallOffer } from '../../../../src/contract/golden/paywall_offer.fixture';
|
|
19
|
+
import { goldenFixture as progressInterstitial } from '../../../../src/contract/golden/progress_interstitial.fixture';
|
|
20
|
+
import { goldenFixture as singleStepChoice } from '../../../../src/contract/golden/single_step_choice.fixture';
|
|
21
|
+
import { goldenFixture as singleStepChoiceEmoji } from '../../../../src/contract/golden/single_step_choice_emoji.fixture';
|
|
22
|
+
import { goldenFixture as socialProof } from '../../../../src/contract/golden/social_proof.fixture';
|
|
23
|
+
import { goldenFixture as subscriptionHandoff } from '../../../../src/contract/golden/subscription_handoff.fixture';
|
|
24
|
+
import { goldenFixture as subscriptionManagement } from '../../../../src/contract/golden/subscription_management.fixture';
|
|
25
|
+
import { goldenFixture as summaryConfirmation } from '../../../../src/contract/golden/summary_confirmation.fixture';
|
|
26
|
+
import { goldenFixture as upsellOffer } from '../../../../src/contract/golden/upsell_offer.fixture';
|
|
27
|
+
import { goldenFixture as valuePropStory } from '../../../../src/contract/golden/value_prop_story.fixture';
|
|
28
28
|
|
|
29
29
|
const fixtures = [
|
|
30
30
|
cancellationOffer,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
|
|
14
14
|
const loadCli = async () => {
|
|
15
|
-
return import('
|
|
15
|
+
return import('../../../src/contract/validate-funnel.cli');
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const diagnostic = createFunnelContractDiagnostic({
|
|
@@ -150,7 +150,7 @@ describe('validate funnel CLI protocol', () => {
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
it('runs through vite-node with one clean stdout protocol object', async () => {
|
|
153
|
-
const templateRoot = path.resolve(fileURLToPath(new URL('
|
|
153
|
+
const templateRoot = path.resolve(fileURLToPath(new URL('../../..', import.meta.url)));
|
|
154
154
|
const viteNode = path.resolve(templateRoot, '../../node_modules/.bin/vite-node');
|
|
155
155
|
const script = path.resolve(templateRoot, 'src/contract/validate-funnel.cli.ts');
|
|
156
156
|
|
|
@@ -23,7 +23,7 @@ describe('checkoutRuntimeConfig', () => {
|
|
|
23
23
|
it('does not invent a checkout SDK publishable key from generic Stripe env', async () => {
|
|
24
24
|
process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY = 'pk_test_generic_runtime_value';
|
|
25
25
|
|
|
26
|
-
const { checkoutRuntimeConfig } = await import('
|
|
26
|
+
const { checkoutRuntimeConfig } = await import('../../../src/runtime/checkout-runtime-config');
|
|
27
27
|
|
|
28
28
|
expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBeNull();
|
|
29
29
|
});
|
|
@@ -43,7 +43,7 @@ describe('checkoutRuntimeConfig', () => {
|
|
|
43
43
|
};
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
const { checkoutRuntimeConfig, resolveClaimbeeCheckoutMode } = await import('
|
|
46
|
+
const { checkoutRuntimeConfig, resolveClaimbeeCheckoutMode } = await import('../../../src/runtime/checkout-runtime-config');
|
|
47
47
|
|
|
48
48
|
expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBe(
|
|
49
49
|
'sdk_pub_claimbee_project',
|
|
@@ -52,7 +52,7 @@ describe('checkoutRuntimeConfig', () => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it('uses the server-provided runtime mode when no preview key is injected', async () => {
|
|
55
|
-
const { resolveClaimbeeCheckoutMode } = await import('
|
|
55
|
+
const { resolveClaimbeeCheckoutMode } = await import('../../../src/runtime/checkout-runtime-config');
|
|
56
56
|
|
|
57
57
|
expect(resolveClaimbeeCheckoutMode('live')).toBe('live');
|
|
58
58
|
expect(resolveClaimbeeCheckoutMode('test')).toBe('test');
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
defaultFunnelStepId,
|
|
5
5
|
resolveConfiguredNextStep,
|
|
6
6
|
resolveRuntimeInitialStepId,
|
|
7
|
-
} from '
|
|
7
|
+
} from '../../../src/runtime/funnel-runtime';
|
|
8
8
|
|
|
9
9
|
describe('funnel runtime', () => {
|
|
10
10
|
it('falls back to the first manifest step when no valid step is requested', () => {
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
getStepContentLocale,
|
|
4
4
|
getStepCountryCode,
|
|
5
|
-
} from '
|
|
5
|
+
} from '../../../src/runtime/step-content-context';
|
|
6
6
|
|
|
7
7
|
describe('step content context helpers', () => {
|
|
8
8
|
it('prefers stored browser language and explicit country when present', () => {
|
package/template_scaffold/{src → tests/src}/runtime/use-funnel-flow-controller.integration.test.ts
RENAMED
|
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
const { posthogFlagsPath } = vi.hoisted(() => ({
|
|
5
5
|
posthogFlagsPath: new URL(
|
|
6
|
-
'
|
|
6
|
+
'../../../../funnel-runtime/src/runtime/posthog-flags.ts',
|
|
7
7
|
import.meta.url,
|
|
8
8
|
).pathname,
|
|
9
9
|
}));
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
resolveExperimentVariant,
|
|
32
32
|
resolveNextStepId,
|
|
33
33
|
} from '@funnelsgrove/runtime';
|
|
34
|
-
import { computeRenderSuspended } from '
|
|
34
|
+
import { computeRenderSuspended } from '../../../src/runtime/use-funnel-flow-controller';
|
|
35
35
|
|
|
36
36
|
const mockedResolveVariant = vi.mocked(resolveExperimentVariant);
|
|
37
37
|
|
|
@@ -187,9 +187,9 @@ describe.skipIf(!insideMonorepo)('PostHog variant resolution (integration)', ()
|
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
it('renders experiment variants on the source URL before normal continuation', () => {
|
|
190
|
-
const adapterSource = readFileSync(new URL('
|
|
190
|
+
const adapterSource = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
|
|
191
191
|
const runtimeSource = readFileSync(
|
|
192
|
-
new URL('
|
|
192
|
+
new URL('../../../../funnel-runtime/src/runtime/use-funnel-flow-controller.ts', import.meta.url),
|
|
193
193
|
'utf8',
|
|
194
194
|
);
|
|
195
195
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { computeRenderSuspended } from '
|
|
3
|
+
import { computeRenderSuspended } from '../../../src/runtime/use-funnel-flow-controller';
|
|
4
4
|
|
|
5
5
|
describe('computeRenderSuspended', () => {
|
|
6
6
|
const experiments = [
|
|
@@ -52,7 +52,7 @@ describe('computeRenderSuspended', () => {
|
|
|
52
52
|
|
|
53
53
|
describe('qualified visitor analytics', () => {
|
|
54
54
|
it('delegates flow behavior to the shared runtime controller with template analytics', () => {
|
|
55
|
-
const source = readFileSync(new URL('
|
|
55
|
+
const source = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
|
|
56
56
|
|
|
57
57
|
expect(source).toContain('useFunnelFlowController as useSharedFunnelFlowController');
|
|
58
58
|
expect(source).toContain('return useSharedFunnelFlowController<FunnelStepId>({');
|
|
@@ -66,7 +66,7 @@ describe('qualified visitor analytics', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it('does not carry a local copy of shared controller internals', () => {
|
|
69
|
-
const source = readFileSync(new URL('
|
|
69
|
+
const source = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
|
|
70
70
|
|
|
71
71
|
expect(source).not.toContain('FIRST_STEP_ENGAGEMENT_THRESHOLD_MS');
|
|
72
72
|
expect(source).not.toContain('apiService.bootstrapSession');
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
buildPaywallCheckoutBillingDetails,
|
|
4
4
|
buildPaywallCheckoutExpressOptions,
|
|
5
5
|
buildPaywallCheckoutPaymentElementOptions,
|
|
6
|
-
} from '
|
|
6
|
+
} from '../../../src/steps/paywall-checkout-options';
|
|
7
7
|
|
|
8
8
|
describe('paywall checkout options', () => {
|
|
9
9
|
it('prioritizes Link and stacked express checkout buttons for the checkout dialog', () => {
|