create-ailk 0.22.0 → 0.24.0
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/README.md +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +68 -5
- package/dist/sync-routes.js +455 -29
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -107,6 +107,17 @@ const VALID_FIELDS = {
|
|
|
107
107
|
const INITIAL_STATE = null;
|
|
108
108
|
|
|
109
109
|
describe("captureLeadAction — valid input", () => {
|
|
110
|
+
it("#8847 D7 — the success state carries the posted name, for the ending's {name}", async () => {
|
|
111
|
+
mockSubmit.mockResolvedValueOnce({
|
|
112
|
+
ok: true,
|
|
113
|
+
body: { id: "lead_abc123", persisted: "db" },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
117
|
+
|
|
118
|
+
expect(result).toStrictEqual({ kind: "success", name: "Jane Smith" });
|
|
119
|
+
});
|
|
120
|
+
|
|
110
121
|
it("returns success state when api-client responds ok", async () => {
|
|
111
122
|
mockSubmit.mockResolvedValueOnce({
|
|
112
123
|
ok: true,
|
|
@@ -270,7 +281,7 @@ describe("captureLeadAction — outcome mapping (#6730)", () => {
|
|
|
270
281
|
|
|
271
282
|
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
272
283
|
|
|
273
|
-
expect(result).toEqual({ kind: "success" });
|
|
284
|
+
expect(result).toEqual({ kind: "success", name: expect.any(String) });
|
|
274
285
|
});
|
|
275
286
|
});
|
|
276
287
|
|
|
@@ -382,7 +393,7 @@ describe("captureLeadAction — visitor forwarding (#7394)", () => {
|
|
|
382
393
|
|
|
383
394
|
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
384
395
|
|
|
385
|
-
expect(result).toEqual({ kind: "success" });
|
|
396
|
+
expect(result).toEqual({ kind: "success", name: expect.any(String) });
|
|
386
397
|
const expected = visitorForwardHeaders(incoming, FORWARD_KEY);
|
|
387
398
|
expect(expected).toEqual({
|
|
388
399
|
"x-ailk-visitor-ip": "203.0.113.5",
|
|
@@ -397,7 +408,7 @@ describe("captureLeadAction — visitor forwarding (#7394)", () => {
|
|
|
397
408
|
|
|
398
409
|
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
399
410
|
|
|
400
|
-
expect(result).toEqual({ kind: "success" });
|
|
411
|
+
expect(result).toEqual({ kind: "success", name: expect.any(String) });
|
|
401
412
|
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
|
402
413
|
expect(mockSubmit.mock.calls[0]).toHaveLength(1);
|
|
403
414
|
});
|
|
@@ -438,7 +449,7 @@ describe("captureLeadAction — consent and source (#8661 D8)", () => {
|
|
|
438
449
|
makeFormData({ ...VALID_FIELDS, source: "flow:contact", subject: "Support" }),
|
|
439
450
|
);
|
|
440
451
|
|
|
441
|
-
expect(result).toEqual({ kind: "success" });
|
|
452
|
+
expect(result).toEqual({ kind: "success", name: expect.any(String) });
|
|
442
453
|
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
|
443
454
|
const body = mockSubmit.mock.calls[0][0] as {
|
|
444
455
|
source: string;
|
|
@@ -505,7 +516,7 @@ describe("captureLeadAction — consent and source (#8661 D8)", () => {
|
|
|
505
516
|
makeFormData({ ...VALID_FIELDS, source: "conversational-intake" }),
|
|
506
517
|
);
|
|
507
518
|
|
|
508
|
-
expect(result).toEqual({ kind: "success" });
|
|
519
|
+
expect(result).toEqual({ kind: "success", name: expect.any(String) });
|
|
509
520
|
const body = mockSubmit.mock.calls[0][0] as { source: string; consent: unknown };
|
|
510
521
|
expect(body.source).toBe("conversational-intake");
|
|
511
522
|
expect(body.consent).toBe(true);
|
|
@@ -611,6 +622,7 @@ describe("captureLeadAction — the per-visitor limit (#8747)", () => {
|
|
|
611
622
|
for (let i = 0; i < 30; i += 1) {
|
|
612
623
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS))).toEqual({
|
|
613
624
|
kind: "success",
|
|
625
|
+
name: expect.any(String),
|
|
614
626
|
});
|
|
615
627
|
}
|
|
616
628
|
expect(mockSubmit).toHaveBeenCalledTimes(30);
|
|
@@ -628,6 +640,7 @@ describe("captureLeadAction — the per-visitor limit (#8747)", () => {
|
|
|
628
640
|
for (let i = 0; i < 31; i += 1) {
|
|
629
641
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS))).toEqual({
|
|
630
642
|
kind: "success",
|
|
643
|
+
name: expect.any(String),
|
|
631
644
|
});
|
|
632
645
|
}
|
|
633
646
|
expect(mockSubmit).toHaveBeenCalledTimes(31);
|
|
@@ -643,6 +656,7 @@ describe("captureLeadAction — the per-visitor limit (#8747)", () => {
|
|
|
643
656
|
mockHeaders.mockResolvedValue(visitor("198.51.100.7"));
|
|
644
657
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS))).toEqual({
|
|
645
658
|
kind: "success",
|
|
659
|
+
name: expect.any(String),
|
|
646
660
|
});
|
|
647
661
|
});
|
|
648
662
|
});
|
|
@@ -705,6 +719,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
705
719
|
|
|
706
720
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toEqual({
|
|
707
721
|
kind: "success",
|
|
722
|
+
name: expect.any(String),
|
|
708
723
|
next: { href: "/sign-in?next=%2Faccount&email=a%40b.example", label: LABEL },
|
|
709
724
|
});
|
|
710
725
|
});
|
|
@@ -715,6 +730,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
715
730
|
|
|
716
731
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toEqual({
|
|
717
732
|
kind: "success",
|
|
733
|
+
name: expect.any(String),
|
|
718
734
|
next: { href: "/fr/sign-in?next=%2Ffr%2Faccount&email=a%40b.example", label: LABEL },
|
|
719
735
|
});
|
|
720
736
|
});
|
|
@@ -724,6 +740,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
724
740
|
|
|
725
741
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toStrictEqual({
|
|
726
742
|
kind: "success",
|
|
743
|
+
name: expect.any(String),
|
|
727
744
|
});
|
|
728
745
|
});
|
|
729
746
|
|
|
@@ -733,6 +750,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
733
750
|
|
|
734
751
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toEqual({
|
|
735
752
|
kind: "success",
|
|
753
|
+
name: expect.any(String),
|
|
736
754
|
next: { href: "/api/hub-session/sign-in?next=%2Faccount&email=a%40b.example", label: LABEL },
|
|
737
755
|
});
|
|
738
756
|
});
|
|
@@ -743,6 +761,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
743
761
|
|
|
744
762
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toStrictEqual({
|
|
745
763
|
kind: "success",
|
|
764
|
+
name: expect.any(String),
|
|
746
765
|
});
|
|
747
766
|
});
|
|
748
767
|
|
|
@@ -752,6 +771,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
752
771
|
|
|
753
772
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toEqual({
|
|
754
773
|
kind: "success",
|
|
774
|
+
name: expect.any(String),
|
|
755
775
|
next: { href: "/sign-in?next=%2Faccount&email=a%40b.example", label: LABEL },
|
|
756
776
|
});
|
|
757
777
|
});
|
|
@@ -761,6 +781,7 @@ describe("captureLeadAction — the account link (#8757 D8)", () => {
|
|
|
761
781
|
|
|
762
782
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(INVITE_FIELDS))).toStrictEqual({
|
|
763
783
|
kind: "success",
|
|
784
|
+
name: expect.any(String),
|
|
764
785
|
});
|
|
765
786
|
});
|
|
766
787
|
});
|
|
@@ -818,6 +839,7 @@ describe("captureLeadAction — a checkout magnet redirects after the write (#87
|
|
|
818
839
|
|
|
819
840
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS))).toStrictEqual({
|
|
820
841
|
kind: "success",
|
|
842
|
+
name: expect.any(String),
|
|
821
843
|
});
|
|
822
844
|
expect(mockRedirect).not.toHaveBeenCalled();
|
|
823
845
|
},
|
|
@@ -830,6 +852,7 @@ describe("captureLeadAction — a checkout magnet redirects after the write (#87
|
|
|
830
852
|
|
|
831
853
|
expect(await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS))).toStrictEqual({
|
|
832
854
|
kind: "success",
|
|
855
|
+
name: expect.any(String),
|
|
833
856
|
});
|
|
834
857
|
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
|
835
858
|
expect(mockRedirect).not.toHaveBeenCalled();
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* captureLeadAction — Server Action paired with ailkCaptureLead MCP tool.
|
|
5
5
|
*
|
|
6
|
-
* Validates contact-form input with
|
|
7
|
-
*
|
|
6
|
+
* Validates contact-form input with its own `contactFormSchema` (the form's
|
|
7
|
+
* field set is the page's, not a wire contract — the `capture_lead` MCP tool
|
|
8
|
+
* takes the route body since #7171), calls POST /v1/leads/submit via
|
|
9
|
+
* @working-theory/api-client, and returns
|
|
8
10
|
* the shared `LeadCaptureForm` block's `LeadCaptureFormState`.
|
|
9
11
|
*
|
|
10
12
|
* Canonical filename: contact.actions.ts (*.actions.ts per ADR 0013 / U0 spec
|
|
@@ -102,11 +104,11 @@ import {
|
|
|
102
104
|
CONTACT_LEAD_SOURCES,
|
|
103
105
|
flowCheckoutOfferingSchema,
|
|
104
106
|
leadSubmitNextSchema,
|
|
105
|
-
submitLeadInputSchema,
|
|
106
107
|
} from "@working-theory/validation";
|
|
107
108
|
import { cookies, headers } from "next/headers";
|
|
108
109
|
import { redirect } from "next/navigation";
|
|
109
110
|
import { getLocale, getTranslations } from "next-intl/server";
|
|
111
|
+
import { z } from "zod";
|
|
110
112
|
|
|
111
113
|
import { accountInviteHref } from "../../../lib/account-invite";
|
|
112
114
|
import { consentVisitorHeader, readConsentVisitor } from "../../../lib/consent-visitor";
|
|
@@ -139,6 +141,14 @@ const DEFAULT_SOURCE = "contact-form";
|
|
|
139
141
|
const CONSENT_REQUIRED = "Please accept the terms to send your message.";
|
|
140
142
|
const MAX_EXTRA_FIELD_LENGTH = 2000;
|
|
141
143
|
|
|
144
|
+
/** The form's four fields and bounds; each field error is zod's own message. */
|
|
145
|
+
const contactFormSchema = z.object({
|
|
146
|
+
name: z.string().min(1).max(200),
|
|
147
|
+
email: z.string().email(),
|
|
148
|
+
message: z.string().max(2000).optional(),
|
|
149
|
+
phone: z.string().max(50).optional(),
|
|
150
|
+
});
|
|
151
|
+
|
|
142
152
|
/**
|
|
143
153
|
* The declared extra fields, or `null` when they exceed the bounds. Non-string
|
|
144
154
|
* entries (files) and Next's own `$ACTION_…` keys are skipped.
|
|
@@ -203,7 +213,10 @@ function checkoutOfferingOf(next: unknown): string | null {
|
|
|
203
213
|
* the API answered `next: { completion: "invite" }` and this site can build
|
|
204
214
|
* one, else exactly `{ kind: "success" }`. Never throws.
|
|
205
215
|
*/
|
|
206
|
-
async function successState(
|
|
216
|
+
async function successState(
|
|
217
|
+
next: unknown,
|
|
218
|
+
email: string,
|
|
219
|
+
): Promise<Extract<LeadCaptureFormState, { kind: "success" }>> {
|
|
207
220
|
const parsed = leadSubmitNextSchema.safeParse(next);
|
|
208
221
|
if (!parsed.success || parsed.data.completion !== "invite") return { kind: "success" };
|
|
209
222
|
let locale = "en";
|
|
@@ -250,7 +263,7 @@ export async function captureLeadAction(
|
|
|
250
263
|
fieldErrors.consent = CONSENT_REQUIRED;
|
|
251
264
|
}
|
|
252
265
|
|
|
253
|
-
const parsed =
|
|
266
|
+
const parsed = contactFormSchema.safeParse(raw);
|
|
254
267
|
if (!parsed.success) {
|
|
255
268
|
for (const issue of parsed.error.issues) {
|
|
256
269
|
const field = String(issue.path[0] ?? "form");
|
|
@@ -321,5 +334,6 @@ export async function captureLeadAction(
|
|
|
321
334
|
// #8757 D11 — after the write, outside any try: redirect() throws by design.
|
|
322
335
|
const checkoutOffering = checkoutOfferingOf(res.body.next);
|
|
323
336
|
if (checkoutOffering !== null) redirect(`/api/checkout/${checkoutOffering}`);
|
|
324
|
-
|
|
337
|
+
// #8847 D7 — the parsed name, which the magnet's ending fills `{name}` with.
|
|
338
|
+
return { ...(await successState(res.body.next, email)), name };
|
|
325
339
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* leg).
|
|
5
5
|
*
|
|
6
6
|
* Three open questions, deliberately keyed `name` / `message` / `email` —
|
|
7
|
-
* the SAME field names `
|
|
7
|
+
* the SAME field names `contactFormSchema` (`./contact.actions.ts`)
|
|
8
8
|
* expects. `ConversationalIntakeWrapper` reads `IntakeEngineState.answers`
|
|
9
9
|
* by these exact ids to build the `FormData` it hands to the existing
|
|
10
10
|
* `captureLeadAction` (`./contact.actions.ts`), so the script's question
|
|
@@ -11,22 +11,25 @@
|
|
|
11
11
|
// API, an older API without the status route, or any other failed read is
|
|
12
12
|
// treated as accepting: the form renders, and its submit reports honestly.
|
|
13
13
|
//
|
|
14
|
-
// The form
|
|
15
|
-
// form magnet (`flows.get("contact")`, the public flow read — the API
|
|
16
|
-
// the workspace, the site alias keyless and a spoke by its key);
|
|
17
|
-
// page's `contact-form` section fields when it declares any; else
|
|
18
|
-
// built-in form: the seeded magnet's own config, its name, email and
|
|
19
|
-
// labels from the `contact` catalog.
|
|
20
|
-
// the
|
|
21
|
-
//
|
|
22
|
-
// the
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
14
|
+
// The form (#8661 D6, #8847 D7), first usable wins: the workspace's Contact
|
|
15
|
+
// form magnet (`flows.get("contact")`, the public flow read — the API
|
|
16
|
+
// resolves the workspace, the site alias keyless and a spoke by its key);
|
|
17
|
+
// else the page's `contact-form` section fields when it declares any; else
|
|
18
|
+
// the built-in form: the seeded magnet's own config, its name, email and
|
|
19
|
+
// message labels from the `contact` catalog. Every visible fact on the page
|
|
20
|
+
// comes from the magnet (#8847): its field step's title is the page's H1 and
|
|
21
|
+
// `<title>`, its body the line under it (`framedSources`), and its ending
|
|
22
|
+
// the success message. The built-in config stands in when the hub cannot be
|
|
23
|
+
// read, so a fresh deploy looks the same either way. The section's own
|
|
24
|
+
// `successMessage` and `submitLabel` apply only when its own fields are
|
|
25
|
+
// used. A magnet-driven post is stamped `source: "flow:contact"`, so it is
|
|
26
|
+
// an Inbox submission of the Contact form; the other two post as
|
|
27
|
+
// `contact-form` (#8747). The page is rendered on every request, but its two
|
|
28
|
+
// hub reads (the magnet and the capture status) are shared across requests
|
|
29
|
+
// for HUB_READ_TTL_MS (`lib/in-page-magnet.ts`, #8731). The terms statement
|
|
30
|
+
// links Terms and Privacy Policy from a page-built map — a magnet label can
|
|
31
|
+
// name a link key, never an href. Under the form, "Prefer email?" and the
|
|
32
|
+
// `_site.mdx` contact email, when the site has one.
|
|
30
33
|
//
|
|
31
34
|
// A `contact-form` section in the `column` variant (the minimalism Contact
|
|
32
35
|
// page, #8661 D12) gets the stacked form look and a Send that stays enabled,
|
|
@@ -35,7 +38,6 @@
|
|
|
35
38
|
// The chat-style intake (ConversationalIntakeWrapper, #4534) remains
|
|
36
39
|
// available to a site that prefers it. JSON-LD and translations handled by
|
|
37
40
|
// renderPage.
|
|
38
|
-
import { flows, leads } from "@working-theory/api-client";
|
|
39
41
|
import type {
|
|
40
42
|
getMdxSource as defaultGetMdxSource,
|
|
41
43
|
getPageSections as defaultGetPageSections,
|
|
@@ -53,22 +55,30 @@ import {
|
|
|
53
55
|
type LeadCaptureFormText,
|
|
54
56
|
localeHref,
|
|
55
57
|
} from "@working-theory/ui";
|
|
58
|
+
import { Link, Text } from "@working-theory/ui/primitives";
|
|
56
59
|
import {
|
|
57
60
|
CONTACT_FORM_FLOW_SLUG,
|
|
58
61
|
contactFormFlowConfig,
|
|
59
62
|
flowLeadSource,
|
|
60
|
-
type FlowConfig,
|
|
61
63
|
} from "@working-theory/validation";
|
|
62
64
|
import type { Metadata } from "next";
|
|
63
65
|
import { getTranslations } from "next-intl/server";
|
|
64
66
|
import type { ReactElement } from "react";
|
|
65
67
|
|
|
66
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
framedSources,
|
|
70
|
+
inPageView,
|
|
71
|
+
readCaptureStatus,
|
|
72
|
+
readServedMagnet,
|
|
73
|
+
type InPageView,
|
|
74
|
+
} from "../../../lib/in-page-magnet";
|
|
75
|
+
|
|
76
|
+
import { contactFormFromConfig, type ContactFormView } from "./contact-form-magnet";
|
|
67
77
|
import { contactFormText } from "./contact-form-text";
|
|
68
78
|
import { captureLeadAction } from "./contact.actions";
|
|
69
79
|
import { readSiteContactEmail } from "./site-contact-email";
|
|
70
80
|
|
|
71
|
-
// #8661 D6 — rendered on every request; the magnet read behind it is shared for HUB_READ_TTL_MS (#8731).
|
|
81
|
+
// #8661 D6 — rendered on every request; the magnet read behind it is shared for HUB_READ_TTL_MS (#8731, lib/in-page-magnet.ts).
|
|
72
82
|
export const dynamic = "force-dynamic";
|
|
73
83
|
|
|
74
84
|
interface Props {
|
|
@@ -77,11 +87,6 @@ interface Props {
|
|
|
77
87
|
getPageSections?: typeof defaultGetPageSections;
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
type CaptureStatus = Extract<
|
|
81
|
-
Awaited<ReturnType<typeof leads.captureStatus>>,
|
|
82
|
-
{ ok: true }
|
|
83
|
-
>["body"]["data"];
|
|
84
|
-
|
|
85
90
|
type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
|
|
86
91
|
type ContactFormSection = Extract<Section, { type: "contact-form" }>;
|
|
87
92
|
|
|
@@ -118,63 +123,43 @@ function builtInFields(text: LeadCaptureFormText, locale: string): ContactFormFi
|
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* views makes one read of each, not one per view.
|
|
124
|
-
*/
|
|
125
|
-
const HUB_READ_TTL_MS = 15_000;
|
|
126
|
-
|
|
127
|
-
const hubReads = new Map<string, { at: number; read: Promise<unknown> }>();
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* `read()`'s result, shared per instance for HUB_READ_TTL_MS. Concurrent calls
|
|
131
|
-
* share one in-flight read. A read that settles to `null` (any failed read,
|
|
132
|
-
* see the two readers below) is shared the same way, so a burst against an
|
|
133
|
-
* unreachable hub is not a burst of retries.
|
|
134
|
-
*/
|
|
135
|
-
function sharedHubRead<T>(key: string, read: () => Promise<T>): Promise<T> {
|
|
136
|
-
const now = Date.now();
|
|
137
|
-
const hit = hubReads.get(key);
|
|
138
|
-
if (hit && now - hit.at < HUB_READ_TTL_MS) return hit.read as Promise<T>;
|
|
139
|
-
const fresh = read();
|
|
140
|
-
hubReads.set(key, { at: now, read: fresh });
|
|
141
|
-
return fresh;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** The API's capture status, or `null` for any failed read (see header). */
|
|
145
|
-
function readCaptureStatus(): Promise<CaptureStatus | null> {
|
|
146
|
-
return sharedHubRead("capture-status", async () => {
|
|
147
|
-
try {
|
|
148
|
-
const res = await leads.captureStatus();
|
|
149
|
-
return res.ok && res.body.ok === true ? res.body.data : null;
|
|
150
|
-
} catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* The served Contact form magnet's config, or `null` for any failed read: a
|
|
158
|
-
* non-2xx (absent, draft, a build with no flow route) or a throw. The
|
|
159
|
-
* property access sits inside the `try`, so a client with no `flows` is a
|
|
160
|
-
* failed read too.
|
|
126
|
+
* The page's view of the Contact form (#8847 D7): the served magnet's when it
|
|
127
|
+
* maps, else the built-in config's. `served` says which.
|
|
161
128
|
*/
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
129
|
+
async function contactView(
|
|
130
|
+
locale: string,
|
|
131
|
+
fallbackHeadline: string,
|
|
132
|
+
): Promise<{ view: InPageView; served: boolean }> {
|
|
133
|
+
const magnet = await readServedMagnet(CONTACT_FORM_FLOW_SLUG);
|
|
134
|
+
const served = magnet ? inPageView(magnet, locale, "contact", fallbackHeadline) : null;
|
|
135
|
+
if (served) return { view: served, served: true };
|
|
136
|
+
// The built-in config always maps: its first step has name and email.
|
|
137
|
+
return {
|
|
138
|
+
view: inPageView(contactFormFlowConfig, locale, "contact", fallbackHeadline)!,
|
|
139
|
+
served: false,
|
|
140
|
+
};
|
|
171
141
|
}
|
|
172
142
|
|
|
173
143
|
export async function generateMetadata({
|
|
174
144
|
params,
|
|
175
|
-
|
|
145
|
+
getMdxSource,
|
|
146
|
+
getPageSections,
|
|
147
|
+
}: Props): Promise<Metadata> {
|
|
176
148
|
const { locale } = await params;
|
|
177
|
-
|
|
149
|
+
const t = await getTranslations({ locale, namespace: "contact" });
|
|
150
|
+
const { view } = await contactView(locale, t("successMessage"));
|
|
151
|
+
// #8847 D7 — the `<title>` is the magnet's step title, like the H1.
|
|
152
|
+
const framed = framedSources({
|
|
153
|
+
getMdxSource: getMdxSource ?? loadMdxSource,
|
|
154
|
+
getPageSections: getPageSections ?? loadPageSections,
|
|
155
|
+
view,
|
|
156
|
+
});
|
|
157
|
+
return renderPageMetadata({
|
|
158
|
+
type: "ContactPage",
|
|
159
|
+
slug: "contact",
|
|
160
|
+
locale,
|
|
161
|
+
getMdxSource: framed.getMdxSource,
|
|
162
|
+
});
|
|
178
163
|
}
|
|
179
164
|
|
|
180
165
|
export default async function Page({
|
|
@@ -183,17 +168,22 @@ export default async function Page({
|
|
|
183
168
|
getPageSections,
|
|
184
169
|
}: Props): Promise<ReactElement> {
|
|
185
170
|
const { locale } = await params;
|
|
186
|
-
const [status, t
|
|
171
|
+
const [status, t] = await Promise.all([
|
|
187
172
|
readCaptureStatus(),
|
|
188
173
|
getTranslations({ locale, namespace: "contact" }),
|
|
189
|
-
readContactMagnet(),
|
|
190
174
|
]);
|
|
191
175
|
const accepting = status?.contact ?? true;
|
|
192
176
|
const contactEmail = readSiteContactEmail();
|
|
193
177
|
const text = contactFormText(t);
|
|
178
|
+
const { view, served } = await contactView(locale, text.success);
|
|
179
|
+
const framed = framedSources({
|
|
180
|
+
getMdxSource: getMdxSource ?? loadMdxSource,
|
|
181
|
+
getPageSections: getPageSections ?? loadPageSections,
|
|
182
|
+
view,
|
|
183
|
+
});
|
|
194
184
|
|
|
195
|
-
const source = await
|
|
196
|
-
const sections = source ? await
|
|
185
|
+
const source = await framed.getMdxSource("ContactPage", "contact", locale);
|
|
186
|
+
const sections = source ? await framed.getPageSections(source.frontmatter) : [];
|
|
197
187
|
const formSection = sections.find(
|
|
198
188
|
(s): s is ContactFormSection => s.type === "contact-form",
|
|
199
189
|
);
|
|
@@ -205,19 +195,23 @@ export default async function Page({
|
|
|
205
195
|
(lead?.type === "hero" && lead.data.variant === "split" ? lead.id : undefined);
|
|
206
196
|
|
|
207
197
|
// #8661 D6 — the magnet's fields, else the section's, else the built-in form.
|
|
208
|
-
const
|
|
209
|
-
const declared =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
198
|
+
const form = view.form as ContactFormView;
|
|
199
|
+
const declared =
|
|
200
|
+
!served && formSection && formSection.data.fields.length > 0 ? formSection : undefined;
|
|
201
|
+
const fields: ContactFormField[] = served
|
|
202
|
+
? form.fields
|
|
203
|
+
: declared
|
|
204
|
+
? sectionFields(declared)
|
|
205
|
+
: builtInFields(text, locale);
|
|
206
|
+
// #8847 D7 — the section's submit label and success message apply only to
|
|
207
|
+
// its own fields; otherwise the magnet's (or the built-in config's) ending.
|
|
208
|
+
const submitLabel = served ? form.submitLabel : declared?.data.submitLabel;
|
|
215
209
|
const column = formSection?.data.variant === "column";
|
|
216
210
|
|
|
217
|
-
const
|
|
211
|
+
const leadForm = accepting ? (
|
|
218
212
|
<LeadCaptureForm
|
|
219
213
|
action={captureLeadAction}
|
|
220
|
-
text={served
|
|
214
|
+
text={served && form.consentStatement ? { ...text, consentStatement: form.consentStatement } : text}
|
|
221
215
|
termsHref={localeHref(locale, "/terms")}
|
|
222
216
|
policies={{
|
|
223
217
|
terms: localeHref(locale, "/terms"),
|
|
@@ -227,7 +221,7 @@ export default async function Page({
|
|
|
227
221
|
fallbackEmail={fallbackEmail}
|
|
228
222
|
fields={fields}
|
|
229
223
|
submitLabel={submitLabel}
|
|
230
|
-
|
|
224
|
+
{...(declared ? { successMessage: declared.data.successMessage } : { ending: view.ending })}
|
|
231
225
|
{...(served && { source: flowLeadSource(CONTACT_FORM_FLOW_SLUG) })}
|
|
232
226
|
{...(column && { look: "column" as const, consentGate: "on-submit" as const })}
|
|
233
227
|
/>
|
|
@@ -241,14 +235,27 @@ export default async function Page({
|
|
|
241
235
|
}}
|
|
242
236
|
/>
|
|
243
237
|
);
|
|
238
|
+
// #8847 D7 — one muted line under the form; absent with no address.
|
|
239
|
+
const surface = (
|
|
240
|
+
<>
|
|
241
|
+
{leadForm}
|
|
242
|
+
{contactEmail !== undefined && (
|
|
243
|
+
<div className="mt-block-tight text-center" data-testid="contact-prefer-email">
|
|
244
|
+
<Text tone="muted" variant="body-sm">
|
|
245
|
+
{t("preferEmail")} <Link href={`mailto:${contactEmail}`}>{contactEmail}</Link>
|
|
246
|
+
</Text>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
250
|
+
);
|
|
244
251
|
|
|
245
252
|
const rendered = await renderPage({
|
|
246
253
|
type: "ContactPage",
|
|
247
254
|
slug: "contact",
|
|
248
255
|
locale,
|
|
249
256
|
translationsNamespace: "contact",
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
getMdxSource: framed.getMdxSource,
|
|
258
|
+
getPageSections: framed.getPageSections,
|
|
252
259
|
...(slotted !== undefined && { sectionSlots: { [slotted]: surface } }),
|
|
253
260
|
});
|
|
254
261
|
return (
|