create-ailk 0.21.0 → 0.23.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/component-catalog.json +186 -8
- package/component-catalog.md +9 -7
- package/dist/add-module.js +20 -14
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +12 -3
- package/dist/derive-architecture.js +20 -8
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +5 -5
- 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 +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +99 -9
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/programmatic.js +19 -5
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +65 -41
- package/dist/spoke-checkout.js +208 -430
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +52 -26
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +187 -13
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +8 -4
- package/dist/topology.js +25 -1
- package/dist/update.js +15 -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/designs/minimalism/DESIGN.md +1 -1
- 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 +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +87 -17
- 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 +3 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- 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/auth.ts +13 -6
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +33 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -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 +1300 -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 +386 -0
- 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 +268 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- 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 +4 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- 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/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +164 -7
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -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 +5 -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 +245 -36
- package/templates/apps/api/src/server.ts +163 -46
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -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 +1383 -133
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +431 -47
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -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 +868 -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 +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -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 +164 -17
- package/templates/apps/api/src/services/plan-offerings.ts +125 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
- package/templates/apps/mcp/CLAUDE.md +22 -14
- 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 +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- 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__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- 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/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +24 -31
- package/templates/apps/web/CLAUDE.md +393 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -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 +141 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
- 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__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -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]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -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]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- 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]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- 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]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- 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/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- 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/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -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 +101 -25
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +16 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +7 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -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/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +71 -20
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -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 +69 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -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 +64 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -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/package.json +1 -1
- package/templates/database/seed.ts +62 -3
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
- 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/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -43,6 +43,11 @@ import {
|
|
|
43
43
|
isSignUpOpen,
|
|
44
44
|
resetSendConfigured,
|
|
45
45
|
} from "./lib/auth-env";
|
|
46
|
+
import {
|
|
47
|
+
signUpAgreementHooks,
|
|
48
|
+
signUpAgreementsPlugin,
|
|
49
|
+
withAuthTransactionCapture,
|
|
50
|
+
} from "./lib/sign-up-agreements";
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
53
|
* Reset-token lifetime (#5962 § D11). One hour — the library's own default,
|
|
@@ -438,9 +443,25 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
438
443
|
};
|
|
439
444
|
|
|
440
445
|
const instance = betterAuth({
|
|
441
|
-
|
|
446
|
+
/**
|
|
447
|
+
* `transaction: true` (#8765 D4): Better Auth opens one `$transaction` for
|
|
448
|
+
* exactly its three `runWithTransaction` sites — `/sign-up/email`,
|
|
449
|
+
* `createOAuthUser` and verification-token consumption — so a user, its
|
|
450
|
+
* account and its sign-up grants commit together or not at all. Every
|
|
451
|
+
* other write stays autocommit. The capture wrapper exposes that
|
|
452
|
+
* transaction's client to the sign-up hooks below; it changes nothing
|
|
453
|
+
* about the transaction itself (lib/sign-up-agreements.ts).
|
|
454
|
+
*/
|
|
455
|
+
database: prismaAdapter(withAuthTransactionCapture(prisma), {
|
|
442
456
|
provider: "postgresql",
|
|
457
|
+
transaction: true,
|
|
443
458
|
}),
|
|
459
|
+
/**
|
|
460
|
+
* The sign-up grants (#8765 D4): the user hook decides whether the new
|
|
461
|
+
* user carries agreements, the account hook writes them on the captured
|
|
462
|
+
* transaction client. No other database hook is installed.
|
|
463
|
+
*/
|
|
464
|
+
databaseHooks: signUpAgreementHooks(),
|
|
444
465
|
secret: resolveSecret(),
|
|
445
466
|
baseURL: process.env.BETTER_AUTH_URL,
|
|
446
467
|
socialProviders: buildSocialProviders(),
|
|
@@ -653,13 +674,18 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
653
674
|
* Sign-in only — the callback mails existing users and nobody else, and
|
|
654
675
|
* `disableSignUp` refuses to create one even for a live token
|
|
655
676
|
* (lib/auth-email.ts).
|
|
677
|
+
*
|
|
678
|
+
* `signUpAgreementsPlugin` always (#8765 D3): while sign-up is open,
|
|
679
|
+
* `/sign-up/email` without every sign-up agreement is refused before the
|
|
680
|
+
* handler looks the address up.
|
|
656
681
|
*/
|
|
657
682
|
plugins: magicLinkActive
|
|
658
683
|
? [
|
|
659
684
|
nextCookies(),
|
|
685
|
+
signUpAgreementsPlugin(),
|
|
660
686
|
magicLink(makeMagicLinkOptions({ send: authEmailTransport })),
|
|
661
687
|
]
|
|
662
|
-
: [nextCookies()],
|
|
688
|
+
: [nextCookies(), signUpAgreementsPlugin()],
|
|
663
689
|
});
|
|
664
690
|
return instance;
|
|
665
691
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* account-invite.test.ts — #8757 D8 / AC-5: the /contact thank-you's account
|
|
3
|
+
* link, by the generated sign-in provider. A site signs in at its own
|
|
4
|
+
* locale-prefixed `/sign-in` and lands on its own `/account`; a hub-sign-in
|
|
5
|
+
* spoke starts at `/api/hub-session/sign-in`; a site with no sign-in gets no
|
|
6
|
+
* link, and neither does an email the hint schema refuses.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let mockProvider: "site" | "hub" | "none" = "site";
|
|
10
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: SIGN_IN_PROVIDER is a generated per-build constant (#8757 D8), not a runtime input; no env or HTTP surface selects it.
|
|
11
|
+
jest.mock("../topology.generated", () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
get SIGN_IN_PROVIDER() {
|
|
14
|
+
return mockProvider;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { accountInviteHref } from "../account-invite";
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
mockProvider = "site";
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("accountInviteHref (#8757 D8)", () => {
|
|
25
|
+
it("site, the default locale: the bare /sign-in with next=/account and the email", () => {
|
|
26
|
+
expect(accountInviteHref("en", "a@b.example")).toBe(
|
|
27
|
+
"/sign-in?next=%2Faccount&email=a%40b.example",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("site, another locale: both the sign-in path and next carry the prefix", () => {
|
|
32
|
+
expect(accountInviteHref("fr", "a@b.example")).toBe(
|
|
33
|
+
"/fr/sign-in?next=%2Ffr%2Faccount&email=a%40b.example",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("site: a plus sign in the address is encoded, never read as a space", () => {
|
|
38
|
+
expect(accountInviteHref("en", "a+b@x.example")).toBe(
|
|
39
|
+
"/sign-in?next=%2Faccount&email=a%2Bb%40x.example",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("hub: the spoke's hub-session start, unprefixed", () => {
|
|
44
|
+
mockProvider = "hub";
|
|
45
|
+
expect(accountInviteHref("fr", "a@b.example")).toBe(
|
|
46
|
+
"/api/hub-session/sign-in?next=%2Faccount&email=a%40b.example",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("none: no link", () => {
|
|
51
|
+
mockProvider = "none";
|
|
52
|
+
expect(accountInviteHref("en", "a@b.example")).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.each(["x\r\n@y.example", "a&next=/evil@b.example", "not-an-email", ""])(
|
|
56
|
+
"an email the hint schema refuses (%j) gives no link, on either provider",
|
|
57
|
+
(email) => {
|
|
58
|
+
expect(accountInviteHref("en", email)).toBeNull();
|
|
59
|
+
mockProvider = "hub";
|
|
60
|
+
expect(accountInviteHref("en", email)).toBeNull();
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* and the absent-header fail-safe — and the same for `isAccountPath()` (#7430).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { AUTH_SEGMENTS, isAccountPath, isAuthPath } from "../auth-routes";
|
|
10
|
+
import { AUTH_SEGMENTS, isAccountPath, isAuthPath, isBookingPath } from "../auth-routes";
|
|
11
11
|
|
|
12
12
|
const LOCALES = ["en", "fr", "de"];
|
|
13
13
|
|
|
@@ -111,3 +111,29 @@ describe("isAccountPath (#7430, spec 7423 D4)", () => {
|
|
|
111
111
|
expect(isAccountPath(null, LOCALES)).toBe(false);
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
// #8843 D10 — the booking pages render without the marketing chrome.
|
|
116
|
+
describe("isBookingPath (#8843)", () => {
|
|
117
|
+
it("matches /meet, a meeting page and /private, bare or locale-prefixed", () => {
|
|
118
|
+
for (const path of ["/meet", "/meet/", "/meet/intro", "/en/meet/intro", "/fr/meet", "/private", "/fr/private"]) {
|
|
119
|
+
expect(isBookingPath(path, LOCALES)).toBe(true);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not match the confirmation page, which keeps the marketing chrome", () => {
|
|
124
|
+
expect(isBookingPath("/meet/confirmed", LOCALES)).toBe(false);
|
|
125
|
+
expect(isBookingPath("/fr/meet/confirmed", LOCALES)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not match a near miss or a marketing path", () => {
|
|
129
|
+
expect(isBookingPath("/meeting", LOCALES)).toBe(false);
|
|
130
|
+
expect(isBookingPath("/privacy", LOCALES)).toBe(false);
|
|
131
|
+
expect(isBookingPath("/blog/meet", LOCALES)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("fails to NOT-a-booking-path for an empty or absent pathname", () => {
|
|
135
|
+
expect(isBookingPath("", LOCALES)).toBe(false);
|
|
136
|
+
expect(isBookingPath(undefined, LOCALES)).toBe(false);
|
|
137
|
+
expect(isBookingPath(null, LOCALES)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The localized policy map (#8765 D9, AC-5): one map, the registry's keys,
|
|
3
|
+
* each path carrying the locale prefix off the default locale.
|
|
4
|
+
*/
|
|
5
|
+
import { consentPoliciesFor } from "../consent-policies";
|
|
6
|
+
|
|
7
|
+
describe("consentPoliciesFor", () => {
|
|
8
|
+
it("is /contact's three destinations for the default locale", () => {
|
|
9
|
+
expect(consentPoliciesFor("en")).toEqual({
|
|
10
|
+
terms: "/terms",
|
|
11
|
+
privacy: "/privacy",
|
|
12
|
+
working_agreement: "/working-agreement",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("prefixes every path for another locale", () => {
|
|
17
|
+
expect(consentPoliciesFor("fr")).toEqual({
|
|
18
|
+
terms: "/fr/terms",
|
|
19
|
+
privacy: "/fr/privacy",
|
|
20
|
+
working_agreement: "/fr/working-agreement",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -69,8 +69,9 @@ describe("deckCandidates (#8687 D3, AC-2)", () => {
|
|
|
69
69
|
]);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it("/careers (
|
|
72
|
+
it("/careers (sections mode since #8849) resolves the route row, then the [slug] fallback", () => {
|
|
73
73
|
expect(deckCandidates("/careers", LOCALES)).toEqual([
|
|
74
|
+
{ type: "CareersList", slug: "careers" },
|
|
74
75
|
{ type: "LandingPage", slug: "careers" },
|
|
75
76
|
{ type: "BlogList", slug: "careers" },
|
|
76
77
|
]);
|
|
@@ -197,10 +198,15 @@ describe("the registered inputsFor (#8687 D3, AC-2)", () => {
|
|
|
197
198
|
const chrome = siteChrome as typeof siteChrome & {
|
|
198
199
|
deckGrounds?: unknown;
|
|
199
200
|
deckPresentation?: true;
|
|
201
|
+
deckAnchors?: "bare";
|
|
200
202
|
};
|
|
201
203
|
expect(result).toEqual({
|
|
202
204
|
...(chrome.deckGrounds !== undefined ? { deckGrounds: chrome.deckGrounds } : {}),
|
|
203
205
|
deckPresentation: chrome.deckPresentation === true,
|
|
206
|
+
// #8773 — the design's slide naming rides along only when it presents and chose bare.
|
|
207
|
+
...(chrome.deckPresentation === true && chrome.deckAnchors === "bare"
|
|
208
|
+
? { deckAnchorStyle: "bare" }
|
|
209
|
+
: {}),
|
|
204
210
|
});
|
|
205
211
|
});
|
|
206
212
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/in-page-magnet.ts — the in-page form shape (#8847 D6, D15; AC-2).
|
|
3
|
+
*
|
|
4
|
+
* Pure mapping and loader wrapping; the two hub reads are mocked at
|
|
5
|
+
* `@working-theory/api-client`, and the Server Action modules are stubbed so
|
|
6
|
+
* the bound action is asserted by identity.
|
|
7
|
+
*/
|
|
8
|
+
import { LeadCaptureForm } from "@working-theory/ui";
|
|
9
|
+
import {
|
|
10
|
+
contactFormFlowConfig,
|
|
11
|
+
newsletterFlowConfig,
|
|
12
|
+
type FlowConfig,
|
|
13
|
+
} from "@working-theory/validation";
|
|
14
|
+
|
|
15
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
16
|
+
flows: { get: jest.fn() },
|
|
17
|
+
leads: { captureStatus: jest.fn() },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: a "use server" boundary the module only binds
|
|
21
|
+
jest.mock("../../app/[locale]/contact/contact.actions", () => ({
|
|
22
|
+
captureLeadAction: jest.fn(),
|
|
23
|
+
}));
|
|
24
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: a "use server" boundary the module only binds
|
|
25
|
+
jest.mock("../newsletter.actions", () => ({
|
|
26
|
+
subscribeNewsletterAction: jest.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { flows } from "@working-theory/api-client";
|
|
30
|
+
|
|
31
|
+
import { captureLeadAction } from "../../app/[locale]/contact/contact.actions";
|
|
32
|
+
import {
|
|
33
|
+
fillEndingName,
|
|
34
|
+
framedSources,
|
|
35
|
+
inPageView,
|
|
36
|
+
readServedMagnet,
|
|
37
|
+
renderInPageMagnet,
|
|
38
|
+
type InPageView,
|
|
39
|
+
} from "../in-page-magnet";
|
|
40
|
+
|
|
41
|
+
const mockFlowsGet = flows.get as jest.Mock;
|
|
42
|
+
|
|
43
|
+
type FieldStep = Extract<FlowConfig["steps"][number], { type: "field" }>;
|
|
44
|
+
|
|
45
|
+
/** A request-a-quote shaped contact magnet: name, email, need. */
|
|
46
|
+
const QUOTE: FlowConfig = {
|
|
47
|
+
slug: "quote",
|
|
48
|
+
defaultLocale: "en",
|
|
49
|
+
steps: [
|
|
50
|
+
{
|
|
51
|
+
key: "quote",
|
|
52
|
+
type: "field",
|
|
53
|
+
copy: { title: { en: "Request a quote" }, body: { en: "Tell us the job." }, cta: { en: "Ask" } },
|
|
54
|
+
fields: [
|
|
55
|
+
{ name: "name", inputType: "text", required: true, mapsTo: "name", copy: { label: { en: "Name" } } },
|
|
56
|
+
{ name: "email", inputType: "email", required: true, mapsTo: "email", copy: { label: { en: "Email" } } },
|
|
57
|
+
{ name: "need", inputType: "textarea", required: false, copy: { label: { en: "What do you need?" } } },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const TEXT = {
|
|
64
|
+
unavailableHeading: "Unavailable",
|
|
65
|
+
unavailableBody: "Write to {email}.",
|
|
66
|
+
unavailableNoEmail: "No email.",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe("fillEndingName (#8841 D1's rule)", () => {
|
|
70
|
+
it("drops {name} with its comma when there is no name, and fills it when there is", () => {
|
|
71
|
+
expect(fillEndingName("Thanks, {name}.", "")).toBe("Thanks.");
|
|
72
|
+
expect(fillEndingName("Thanks, {name}.", "Ada")).toBe("Thanks, Ada.");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("inPageView — the form comes from the caller (C1)", () => {
|
|
77
|
+
it("a quote-shaped config asked for as contact maps its fields, title and body", () => {
|
|
78
|
+
const view = inPageView(QUOTE, "en", "contact")!;
|
|
79
|
+
expect(view.title).toBe("Request a quote");
|
|
80
|
+
expect(view.body).toBe("Tell us the job.");
|
|
81
|
+
expect(view.stepKey).toBe("quote");
|
|
82
|
+
expect("fields" in view.form && view.form.fields.map((f) => f.name)).toEqual(["name", "email", "need"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("the newsletter config asked for as contact is null — it has no name field", () => {
|
|
86
|
+
expect(inPageView(newsletterFlowConfig, "en", "contact")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("the newsletter shape served at another slug maps when the caller asks for newsletter", () => {
|
|
90
|
+
const updates = { ...newsletterFlowConfig, slug: "updates" };
|
|
91
|
+
const view = inPageView(updates, "en", "newsletter")!;
|
|
92
|
+
expect(view.form).toMatchObject({
|
|
93
|
+
kind: "newsletter",
|
|
94
|
+
emailField: "email",
|
|
95
|
+
label: "Email",
|
|
96
|
+
placeholder: "you@example.com",
|
|
97
|
+
consent: { field: "terms", purposes: ["newsletter"], help: "No spam. Unsubscribe any time." },
|
|
98
|
+
submitLabel: "Subscribe",
|
|
99
|
+
});
|
|
100
|
+
expect(view.ending).toEqual({
|
|
101
|
+
kind: "screen",
|
|
102
|
+
headline: "Check your inbox",
|
|
103
|
+
body: "Your welcome email is on its way.",
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("a newsletter view is null with any other field to fill in, or a second step", () => {
|
|
108
|
+
const extra = JSON.parse(JSON.stringify(newsletterFlowConfig)) as FlowConfig;
|
|
109
|
+
(extra.steps[0] as FieldStep).fields.push({
|
|
110
|
+
name: "company",
|
|
111
|
+
inputType: "text",
|
|
112
|
+
copy: { label: { en: "Company" } },
|
|
113
|
+
});
|
|
114
|
+
expect(inPageView(extra, "en", "newsletter")).toBeNull();
|
|
115
|
+
const twoSteps = { ...newsletterFlowConfig, steps: [...newsletterFlowConfig.steps, ...QUOTE.steps] };
|
|
116
|
+
expect(inPageView(twoSteps, "en", "newsletter")).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("the Contact form's ending is its config's; a config with none ends on the caller's line", () => {
|
|
120
|
+
expect(inPageView(contactFormFlowConfig, "en", "contact")!.ending).toEqual({
|
|
121
|
+
kind: "screen",
|
|
122
|
+
headline: "Message sent.",
|
|
123
|
+
body: "Thanks for getting in touch. I reply within one business day.",
|
|
124
|
+
});
|
|
125
|
+
expect(inPageView(QUOTE, "en", "contact", "Thank you!")!.ending).toEqual({
|
|
126
|
+
kind: "screen",
|
|
127
|
+
headline: "Thank you!",
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("an off-site redirect or button is dropped, never rendered (security review)", () => {
|
|
132
|
+
const offSite = {
|
|
133
|
+
...QUOTE,
|
|
134
|
+
ending: { default: { kind: "redirect" as const, href: "https://evil.example/x" } },
|
|
135
|
+
} as FlowConfig;
|
|
136
|
+
expect(inPageView(offSite, "en", "contact", "Thanks")!.ending).toEqual({ kind: "screen", headline: "Thanks" });
|
|
137
|
+
const buttons = {
|
|
138
|
+
...QUOTE,
|
|
139
|
+
ending: {
|
|
140
|
+
default: {
|
|
141
|
+
kind: "screen" as const,
|
|
142
|
+
headline: { en: "Done" },
|
|
143
|
+
ctas: [
|
|
144
|
+
{ label: { en: "Evil" }, href: "//evil.example" },
|
|
145
|
+
{ label: { en: "Home" }, href: "/" },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
} as FlowConfig;
|
|
150
|
+
expect(inPageView(buttons, "en", "contact")!.ending).toEqual({
|
|
151
|
+
kind: "screen",
|
|
152
|
+
headline: "Done",
|
|
153
|
+
ctas: [{ label: "Home", href: "/" }],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("a redirect ending and a screen's buttons are localized", () => {
|
|
158
|
+
const redirect = { ...QUOTE, ending: { default: { kind: "redirect" as const, href: "/thanks" } } };
|
|
159
|
+
expect(inPageView(redirect, "fr", "contact")!.ending).toEqual({ kind: "redirect", href: "/fr/thanks" });
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("framedSources (D6)", () => {
|
|
164
|
+
const source = { frontmatter: { type: "ContactPage", slug: "contact", title: "Old title" }, body: "" };
|
|
165
|
+
const view: Pick<InPageView, "title" | "body"> = { title: "Say hello", body: "Two days, tops." };
|
|
166
|
+
|
|
167
|
+
it("with no contact-form section both loaders' output is unchanged", async () => {
|
|
168
|
+
const sections = [{ id: "faq", type: "faq", data: { items: [] } }];
|
|
169
|
+
const framed = framedSources({
|
|
170
|
+
getMdxSource: jest.fn().mockResolvedValue(source),
|
|
171
|
+
getPageSections: jest.fn().mockResolvedValue(sections),
|
|
172
|
+
view,
|
|
173
|
+
});
|
|
174
|
+
expect(await framed.getMdxSource("ContactPage", "contact", "en")).toBe(source);
|
|
175
|
+
expect(await framed.getPageSections(source.frontmatter)).toBe(sections);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("a leading contact-form section takes the view's title and body, and so does the frontmatter title", async () => {
|
|
179
|
+
const sections = [
|
|
180
|
+
{ id: "contact", type: "contact-form", data: { variant: "column", headline: "Old", description: "Old line", fields: [] } },
|
|
181
|
+
];
|
|
182
|
+
const framed = framedSources({
|
|
183
|
+
getMdxSource: jest.fn().mockResolvedValue(source),
|
|
184
|
+
getPageSections: jest.fn().mockResolvedValue(sections),
|
|
185
|
+
view,
|
|
186
|
+
});
|
|
187
|
+
expect((await framed.getMdxSource("ContactPage", "contact", "en"))!.frontmatter.title).toBe("Say hello");
|
|
188
|
+
expect((await framed.getPageSections(source.frontmatter))[0]!.data).toMatchObject({
|
|
189
|
+
headline: "Say hello",
|
|
190
|
+
description: "Two days, tops.",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("a view with no body deletes the section's description", async () => {
|
|
195
|
+
const sections = [{ id: "c", type: "contact-form", data: { description: "Old line", fields: [] } }];
|
|
196
|
+
const framed = framedSources({
|
|
197
|
+
getMdxSource: jest.fn().mockResolvedValue(source),
|
|
198
|
+
getPageSections: jest.fn().mockResolvedValue(sections),
|
|
199
|
+
view: { title: "Say hello" },
|
|
200
|
+
});
|
|
201
|
+
expect((await framed.getPageSections(source.frontmatter))[0]!.data).not.toHaveProperty("description");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("renderInPageMagnet (D15)", () => {
|
|
206
|
+
it("a quote magnet asked for as contact binds captureLeadAction, stamped with its flow source", () => {
|
|
207
|
+
const view = inPageView(QUOTE, "en", "contact")!;
|
|
208
|
+
const element = renderInPageMagnet({
|
|
209
|
+
slug: "quote",
|
|
210
|
+
locale: "en",
|
|
211
|
+
view,
|
|
212
|
+
text: { ...TEXT } as never,
|
|
213
|
+
accepting: true,
|
|
214
|
+
});
|
|
215
|
+
expect(element.type).toBe(LeadCaptureForm);
|
|
216
|
+
expect(element.props).toMatchObject({
|
|
217
|
+
action: captureLeadAction,
|
|
218
|
+
// captureLeadAction keeps only CONTACT_LEAD_SOURCES, so this lands as contact-form.
|
|
219
|
+
source: "flow:quote",
|
|
220
|
+
look: "column",
|
|
221
|
+
ending: view.ending,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("capture off is ContactFallback with the site's email", () => {
|
|
226
|
+
const element = renderInPageMagnet({
|
|
227
|
+
slug: "quote",
|
|
228
|
+
locale: "en",
|
|
229
|
+
view: inPageView(QUOTE, "en", "contact")!,
|
|
230
|
+
text: { ...TEXT } as never,
|
|
231
|
+
accepting: false,
|
|
232
|
+
fallbackEmail: "hi@x.co",
|
|
233
|
+
});
|
|
234
|
+
expect(element.props).toMatchObject({ email: "hi@x.co" });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("a config with no ending (review M1)", () => {
|
|
239
|
+
it("has no ending unless the caller gives a fallback line", () => {
|
|
240
|
+
expect(inPageView(QUOTE, "en", "contact")).not.toHaveProperty("ending");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("readServedMagnet", () => {
|
|
245
|
+
it("a hub that never answers is null after the deadline, not a hung page (review H2)", async () => {
|
|
246
|
+
jest.useFakeTimers();
|
|
247
|
+
try {
|
|
248
|
+
mockFlowsGet.mockReturnValueOnce(new Promise(() => undefined));
|
|
249
|
+
const read = readServedMagnet("stalled");
|
|
250
|
+
jest.advanceTimersByTime(2_000);
|
|
251
|
+
expect(await read).toBeNull();
|
|
252
|
+
} finally {
|
|
253
|
+
jest.useRealTimers();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("is the served config, and null for a non-2xx or a throw", async () => {
|
|
258
|
+
mockFlowsGet.mockResolvedValueOnce({ ok: true, status: 200, body: QUOTE });
|
|
259
|
+
expect(await readServedMagnet("quote")).toBe(QUOTE);
|
|
260
|
+
mockFlowsGet.mockResolvedValueOnce({ ok: false, status: 404, body: {} });
|
|
261
|
+
expect(await readServedMagnet("absent")).toBeNull();
|
|
262
|
+
mockFlowsGet.mockRejectedValueOnce(new Error("hub down"));
|
|
263
|
+
expect(await readServedMagnet("down")).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #8840 D10, AC-4 — the magnet walk predicate the layout drops the chrome on.
|
|
3
|
+
*/
|
|
4
|
+
import { isMagnetWalkPath } from "../magnet-routes";
|
|
5
|
+
|
|
6
|
+
const LOCALES = ["en", "fr"];
|
|
7
|
+
|
|
8
|
+
describe("isMagnetWalkPath (#8840 D10)", () => {
|
|
9
|
+
it.each([
|
|
10
|
+
"/diagnostic/start",
|
|
11
|
+
"/fr/diagnostic/start",
|
|
12
|
+
"/flows/a/start",
|
|
13
|
+
"/careers/apply",
|
|
14
|
+
"/careers/writer/apply",
|
|
15
|
+
"/fr/careers/writer/apply",
|
|
16
|
+
"/diagnostic/start?step=2",
|
|
17
|
+
])("is a walk: %s", (path) => {
|
|
18
|
+
expect(isMagnetWalkPath(path, LOCALES)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
"/diagnostic",
|
|
23
|
+
"/careers",
|
|
24
|
+
"/careers/writer",
|
|
25
|
+
"/start-here",
|
|
26
|
+
"/fr",
|
|
27
|
+
"/",
|
|
28
|
+
"",
|
|
29
|
+
"/apply",
|
|
30
|
+
"/careers/a/b/apply",
|
|
31
|
+
])("is not a walk: %j", (path) => {
|
|
32
|
+
expect(isMagnetWalkPath(path, LOCALES)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("answers false for an absent pathname", () => {
|
|
36
|
+
expect(isMagnetWalkPath(undefined, LOCALES)).toBe(false);
|
|
37
|
+
expect(isMagnetWalkPath(null, LOCALES)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("treats a prefix that is not an enabled locale as a first segment", () => {
|
|
41
|
+
expect(isMagnetWalkPath("/de/careers/apply", LOCALES)).toBe(false);
|
|
42
|
+
expect(isMagnetWalkPath("/de/start", LOCALES)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("isMagnetWalkPath — the deliverable pages (#8848 D9, AC-4)", () => {
|
|
47
|
+
it.each(["/en/reports/q3", "/reports/q3", "/reports/q3/start", "/fr/reports/q3/start"])(
|
|
48
|
+
"is chrome-free: %s",
|
|
49
|
+
(path) => {
|
|
50
|
+
expect(isMagnetWalkPath(path, ["en", "fr"])).toBe(true);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
it.each(["/reports", "/en/reports", "/en/reports/"])("keeps the index's chrome: %s", (path) => {
|
|
55
|
+
expect(isMagnetWalkPath(path, ["en", "fr"])).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|