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
package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -87,7 +87,7 @@ describe("IntegrationsPage — snapshot regression", () => {
|
|
|
87
87
|
});
|
|
88
88
|
const { container } = render(element);
|
|
89
89
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
90
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Integrations</h1><p data-testid="integrations-description" class="mt-block-tight text-lead">First-party integrations for AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
90
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Integrations</h1><p data-testid="integrations-description" class="mt-block-tight text-lead">First-party integrations for AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-remote" data-source="
|
|
91
91
|
Body."></div></div></article></div></div></div></main>"
|
|
92
92
|
`);
|
|
93
93
|
});
|
|
@@ -110,7 +110,7 @@ describe("IntegrationSlugPage — snapshot regression", () => {
|
|
|
110
110
|
});
|
|
111
111
|
const { container } = render(element);
|
|
112
112
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
113
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">OpenAI API</h1><p data-testid="integration-description" class="mt-block-tight text-lead">Connect AILK to OpenAI.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
113
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">OpenAI API</h1><p data-testid="integration-description" class="mt-block-tight text-lead">Connect AILK to OpenAI.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-remote" data-source="
|
|
114
114
|
Body."></div></div></article></div></div></div></main>"
|
|
115
115
|
`);
|
|
116
116
|
});
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "@working-theory/ui/chrome";
|
|
40
40
|
import {
|
|
41
41
|
cookiePurpose,
|
|
42
|
+
NEWSLETTER_FLOW_SLUG,
|
|
42
43
|
resolveConsentStatement,
|
|
43
44
|
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
44
45
|
} from "@working-theory/validation";
|
|
@@ -58,13 +59,21 @@ import {
|
|
|
58
59
|
SignedInNav,
|
|
59
60
|
type SignedInNavProps,
|
|
60
61
|
} from "../../components/signed-in-nav";
|
|
61
|
-
import { isAccountPath, isAuthPath } from "../../lib/auth-routes";
|
|
62
|
+
import { isAccountPath, isAuthPath, isBookingPath } from "../../lib/auth-routes";
|
|
62
63
|
import { isClosingFooterPath } from "../../lib/closing-footer-path";
|
|
64
|
+
import { pageOptsIntoDeck } from "../../lib/deck-page";
|
|
65
|
+
import {
|
|
66
|
+
inPageView,
|
|
67
|
+
newsletterSignup,
|
|
68
|
+
readCaptureStatus,
|
|
69
|
+
readServedMagnet,
|
|
70
|
+
} from "../../lib/in-page-magnet";
|
|
63
71
|
// #8687 D5 — the per-page deck opt-in. Importing this module registers the
|
|
64
72
|
// deck-page seam with page-renderer at module load (D3), the same
|
|
65
73
|
// precedent as `../../lib/site-identity` and `../../lib/listings` below.
|
|
66
|
-
import { pageOptsIntoDeck } from "../../lib/deck-page";
|
|
67
74
|
import { assertEnabledLocale } from "../../lib/locale-guard";
|
|
75
|
+
import { isMagnetWalkPath } from "../../lib/magnet-routes";
|
|
76
|
+
import { SCHEDULING_PAGES } from "../../lib/modules.generated";
|
|
68
77
|
import { footerColumns, navItems } from "../../lib/nav-config";
|
|
69
78
|
import { getOgImageUrl } from "../../lib/og";
|
|
70
79
|
import { backLinkHref } from "../../lib/profile-back-link";
|
|
@@ -268,6 +277,27 @@ interface Props {
|
|
|
268
277
|
params: Promise<{ locale: string }>;
|
|
269
278
|
}
|
|
270
279
|
|
|
280
|
+
/**
|
|
281
|
+
* #8847 D13 — the footer's newsletter signup: the newsletter magnet's compact
|
|
282
|
+
* `NewsletterFormInline`, only when the magnet is served (live), maps as a
|
|
283
|
+
* one-field signup, and the API accepts signups. The seeded built-in config
|
|
284
|
+
* never fills the footer. Both reads are shared for 15 seconds per instance.
|
|
285
|
+
*/
|
|
286
|
+
async function footerNewsletter(locale: string): Promise<ReactElement | undefined> {
|
|
287
|
+
const [magnet, status] = await Promise.all([
|
|
288
|
+
readServedMagnet(NEWSLETTER_FLOW_SLUG),
|
|
289
|
+
readCaptureStatus(),
|
|
290
|
+
]);
|
|
291
|
+
if (magnet === null || status?.newsletter !== true) return undefined;
|
|
292
|
+
const view = inPageView(magnet, locale, "newsletter");
|
|
293
|
+
if (view === null || !("kind" in view.form)) return undefined;
|
|
294
|
+
return (
|
|
295
|
+
<div data-testid="footer-newsletter">
|
|
296
|
+
{newsletterSignup({ ...view, form: view.form }, { locale, compact: true })}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
271
301
|
export default async function LocaleLayout({
|
|
272
302
|
children,
|
|
273
303
|
params,
|
|
@@ -291,6 +321,8 @@ export default async function LocaleLayout({
|
|
|
291
321
|
// Null when signed out or auth is unset.
|
|
292
322
|
const session = await readSession(await headers());
|
|
293
323
|
|
|
324
|
+
const footerSignup = await footerNewsletter(locale);
|
|
325
|
+
|
|
294
326
|
// Resolve nav labels server-side so Nav + Footer components stay
|
|
295
327
|
// i18n-hook-free and are testable without a NextIntlClientProvider wrapper.
|
|
296
328
|
const tNav = await getTranslations({ locale, namespace: "nav" });
|
|
@@ -531,6 +563,22 @@ export default async function LocaleLayout({
|
|
|
531
563
|
// fail-safe direction: an absent `x-pathname` keeps the full frame.
|
|
532
564
|
const isAccountRoute = isAccountPath(pathname, enabledLocales);
|
|
533
565
|
|
|
566
|
+
// The booking branch (#8843 D10) — the scheduling module's /meet,
|
|
567
|
+
// /meet/<slug> and /private render chrome-free: the page draws its own
|
|
568
|
+
// back link (the index) or none (a meeting page). Only where the module is
|
|
569
|
+
// installed (`SCHEDULING_PAGES`), so a Free site's /meet placeholder keeps
|
|
570
|
+
// the marketing frame; /meet/confirmed keeps it too. The consent Banner
|
|
571
|
+
// stays, as on the auth branch. The same fail-safe direction: an absent
|
|
572
|
+
// `x-pathname` keeps the full frame.
|
|
573
|
+
const isBookingRoute = SCHEDULING_PAGES && isBookingPath(pathname, enabledLocales);
|
|
574
|
+
|
|
575
|
+
// #8840 D10 — a magnet's walk (`/<magnet>/start`, `/careers/apply`,
|
|
576
|
+
// `/careers/<posting>/apply`) renders full-screen: the auth branch's frame,
|
|
577
|
+
// no Nav and no Footer, and it KEEPS the cookie consent banner — a visitor
|
|
578
|
+
// can land straight on a walk, and analytics must not run on a page that
|
|
579
|
+
// never asked (SEC-8840-A). The same fail-safe direction.
|
|
580
|
+
const isMagnetWalkRoute = isMagnetWalkPath(pathname, enabledLocales);
|
|
581
|
+
|
|
534
582
|
// #8687 D5 — a page whose frontmatter says `layout: deck` presents as the
|
|
535
583
|
// deck home does, without the home's landing (no deckHero, no bare bar).
|
|
536
584
|
// Never the home itself, and never on the auth or account branches, which
|
|
@@ -539,6 +587,7 @@ export default async function LocaleLayout({
|
|
|
539
587
|
!isDeckHome &&
|
|
540
588
|
!isAuthRoute &&
|
|
541
589
|
!isAccountRoute &&
|
|
590
|
+
!isMagnetWalkRoute &&
|
|
542
591
|
(await pageOptsIntoDeck({ pathname, locale }));
|
|
543
592
|
|
|
544
593
|
// Every Nav prop that is safe to serialize — shared verbatim by the
|
|
@@ -717,7 +766,7 @@ export default async function LocaleLayout({
|
|
|
717
766
|
* emitted unconditionally in OSS forks; paying customers install
|
|
718
767
|
* @working-theory/branding and gate it in their own layout.
|
|
719
768
|
*/}
|
|
720
|
-
{isAuthRoute || isAccountRoute ? (
|
|
769
|
+
{isAuthRoute || isAccountRoute || isBookingRoute || isMagnetWalkRoute ? (
|
|
721
770
|
/*
|
|
722
771
|
* The (auth) branch (#5963): the five auth paths render their own
|
|
723
772
|
* `(auth)/layout.tsx` — AuthShell — with no Nav and no marketing
|
|
@@ -726,7 +775,8 @@ export default async function LocaleLayout({
|
|
|
726
775
|
* favicons, NextIntlClientProvider, the consent Banner and
|
|
727
776
|
* Telemetry) is shared by both branches and written once. The
|
|
728
777
|
* account paths (#7430) take this branch too, and also skip the
|
|
729
|
-
* consent Banner below.
|
|
778
|
+
* consent Banner below. The booking paths (#8843 D10) and a magnet
|
|
779
|
+
* walk (#8840 D10) take it and keep the Banner.
|
|
730
780
|
*/
|
|
731
781
|
children
|
|
732
782
|
) : (
|
|
@@ -869,6 +919,7 @@ export default async function LocaleLayout({
|
|
|
869
919
|
{...(isMinimalChrome ? { chromeStyle: "minimal" as const } : {})}
|
|
870
920
|
// #7742 D2 — no theme switch under `colorMode: light`.
|
|
871
921
|
showThemeToggle={!pinLight}
|
|
922
|
+
{...(footerSignup !== undefined && { subscribe: footerSignup })}
|
|
872
923
|
bottomBarAlign="content"
|
|
873
924
|
copyrightText={copyrightText}
|
|
874
925
|
legalLinks={legalLinks}
|
|
@@ -1,101 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* newsletter/page.tsx —
|
|
3
|
-
* (#6730 D9).
|
|
2
|
+
* newsletter/page.tsx — the newsletter magnet is the only source (#8847 D11,
|
|
3
|
+
* AC-6), and a signup only when there is somewhere to keep it (#6730 D9).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Renders the REAL `NewsletterFormInline` and binds the REAL
|
|
6
|
+
* `subscribeNewsletterAction`. Mocked boundaries: `@working-theory/api-client`
|
|
7
|
+
* (`flows.get`, `flows.submitStep`, `leads.captureStatus`, `leads.subscribe`),
|
|
8
|
+
* `next/headers`, `next-intl/server` (over the real English catalog), the
|
|
9
|
+
* `_site.mdx` contact-email read, and the page-renderer's content loaders
|
|
10
|
+
* (injected).
|
|
9
11
|
*/
|
|
10
12
|
import React from "react";
|
|
11
|
-
import { render, screen } from "@testing-library/react";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
data: { action?: string; primaryCta?: { href: string } };
|
|
26
|
-
}>;
|
|
27
|
-
}) =>
|
|
28
|
-
React.createElement(
|
|
29
|
-
"div",
|
|
30
|
-
{ "data-testid": "section-renderer" },
|
|
31
|
-
sections.map((s) =>
|
|
32
|
-
s.type === "newsletter"
|
|
33
|
-
? React.createElement(
|
|
34
|
-
"form",
|
|
35
|
-
{ key: s.id, "data-section-id": s.id, action: s.data.action },
|
|
36
|
-
"signup",
|
|
37
|
-
)
|
|
38
|
-
: React.createElement(
|
|
39
|
-
"div",
|
|
40
|
-
{ key: s.id, "data-section-id": s.id },
|
|
41
|
-
s.data.primaryCta
|
|
42
|
-
? React.createElement("a", { href: s.data.primaryCta.href }, "cta")
|
|
43
|
-
: null,
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
),
|
|
47
|
-
localeHref: (locale: string, path: string) => `/${locale}${path}`,
|
|
48
|
-
LeadCaptureForm: (props: { fields?: Array<{ name: string; type: string }> }) =>
|
|
49
|
-
React.createElement("div", {
|
|
50
|
-
"data-testid": "lead-capture-form",
|
|
51
|
-
"data-fields": (props.fields ?? []).map((f) => `${f.name}:${f.type}`).join(","),
|
|
52
|
-
}),
|
|
13
|
+
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
14
|
+
import { newsletterFlowConfig, type FlowConfig } from "@working-theory/validation";
|
|
15
|
+
|
|
16
|
+
jest.mock("next/navigation", () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
notFound: jest.fn(() => {
|
|
19
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
MDXRemote: ({ source }: { source: string }) =>
|
|
26
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
53
27
|
}));
|
|
54
28
|
|
|
55
29
|
jest.mock("@working-theory/api-client", () => ({
|
|
56
|
-
|
|
30
|
+
flows: { get: jest.fn(), submitStep: jest.fn() },
|
|
31
|
+
leads: { captureStatus: jest.fn(), submit: jest.fn(), subscribe: jest.fn() },
|
|
32
|
+
visitorForwardHeaders: () => undefined,
|
|
57
33
|
}));
|
|
58
34
|
|
|
59
|
-
jest.mock("next
|
|
60
|
-
|
|
35
|
+
jest.mock("next/headers", () => ({
|
|
36
|
+
__esModule: true,
|
|
37
|
+
headers: jest.fn().mockResolvedValue({ get: () => null }),
|
|
38
|
+
cookies: jest.fn().mockResolvedValue({ get: () => undefined }),
|
|
61
39
|
}));
|
|
62
40
|
|
|
63
|
-
//
|
|
64
|
-
jest.mock("
|
|
65
|
-
|
|
41
|
+
// The real English catalog, so the page's own strings render as they ship.
|
|
42
|
+
jest.mock("next-intl/server", () => {
|
|
43
|
+
const messages = jest.requireActual("../../../../messages/en.json") as Record<
|
|
44
|
+
string,
|
|
45
|
+
Record<string, unknown>
|
|
46
|
+
>;
|
|
47
|
+
return {
|
|
48
|
+
getLocale: async () => "en",
|
|
49
|
+
getTranslations: async (arg: string | { namespace: string }) => {
|
|
50
|
+
const ns = messages[typeof arg === "string" ? arg : arg.namespace] ?? {};
|
|
51
|
+
return (key: string, values?: Record<string, string>) =>
|
|
52
|
+
String(ns[key] ?? key).replace(/\{(\w+)\}/g, (m, name: string) => values?.[name] ?? m);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: the real read is a node:fs frontmatter parse of content/_site.mdx; narrowest possible scope
|
|
58
|
+
jest.mock("../../contact/site-contact-email", () => ({
|
|
59
|
+
readSiteContactEmail: jest.fn().mockReturnValue("hi@x.co"),
|
|
66
60
|
}));
|
|
67
61
|
|
|
68
|
-
import { leads } from "@working-theory/api-client";
|
|
62
|
+
import { flows, leads } from "@working-theory/api-client";
|
|
69
63
|
|
|
64
|
+
import * as pageModule from "../page";
|
|
70
65
|
import NewsletterPage from "../page";
|
|
71
66
|
|
|
67
|
+
const mockFlowsGet = flows.get as jest.Mock;
|
|
68
|
+
const mockSubmitStep = flows.submitStep as jest.Mock;
|
|
72
69
|
const mockCaptureStatus = leads.captureStatus as jest.Mock;
|
|
70
|
+
|
|
73
71
|
const status = (newsletter: boolean) => ({
|
|
74
72
|
ok: true,
|
|
75
73
|
status: 200,
|
|
76
74
|
body: { ok: true, data: { contact: true, newsletter } },
|
|
77
75
|
});
|
|
78
76
|
|
|
77
|
+
/** The served magnet: the stored config minus its completion email. */
|
|
78
|
+
function served(config: FlowConfig) {
|
|
79
|
+
const { completionEmail: _omitted, ...body } = config;
|
|
80
|
+
return { ok: true, status: 200, body };
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
const source = {
|
|
80
84
|
frontmatter: {
|
|
81
85
|
type: "NewsletterSignup" as const,
|
|
82
86
|
slug: "newsletter",
|
|
83
|
-
title: "Newsletter",
|
|
87
|
+
title: "{Newsletter}",
|
|
84
88
|
description: "What subscribers get.",
|
|
85
89
|
},
|
|
86
90
|
body: "",
|
|
87
91
|
};
|
|
88
92
|
|
|
89
|
-
/**
|
|
90
|
-
const
|
|
91
|
-
{ id: "
|
|
92
|
-
{ id: "signup", type: "newsletter", data: { action: "/v1/leads/subscribe", source: "newsletter" } },
|
|
93
|
-
{ id: "faq", type: "faq", data: { items: [] } },
|
|
93
|
+
/** The starter's seed (#8847 D14): one column contact-form section. */
|
|
94
|
+
const STARTER_SECTIONS = [
|
|
95
|
+
{ id: "signup", type: "contact-form", data: { variant: "column", fields: [] } },
|
|
94
96
|
];
|
|
95
97
|
|
|
96
|
-
/**
|
|
97
|
-
const
|
|
98
|
-
{ id: "hero", type: "hero", data: { headline: "News" } },
|
|
98
|
+
/** `_base`'s pre-#8847 content: a hero whose CTA points at `#signup`, the signup section, an FAQ. */
|
|
99
|
+
const LEGACY_SECTIONS = [
|
|
100
|
+
{ id: "hero", type: "hero", data: { headline: "News", primaryCta: { label: "Subscribe", href: "#signup" } } },
|
|
101
|
+
{ id: "signup", type: "newsletter", data: { action: "/v1/leads/subscribe", source: "newsletter" } },
|
|
99
102
|
{ id: "faq", type: "faq", data: { items: [] } },
|
|
100
103
|
];
|
|
101
104
|
|
|
@@ -108,59 +111,126 @@ async function renderWith(sections: unknown[]) {
|
|
|
108
111
|
return render(element);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
// The page shares its hub reads for a few seconds, per module instance. Each
|
|
115
|
+
// test starts an hour later so none sees another's read.
|
|
116
|
+
let clock = 1_900_000_000_000;
|
|
117
|
+
jest.spyOn(Date, "now").mockImplementation(() => clock);
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
jest.clearAllMocks();
|
|
121
|
+
clock += 60 * 60 * 1000;
|
|
122
|
+
mockCaptureStatus.mockResolvedValue(status(true));
|
|
123
|
+
mockFlowsGet.mockResolvedValue(served(newsletterFlowConfig));
|
|
124
|
+
});
|
|
116
125
|
|
|
117
|
-
|
|
126
|
+
describe("NewsletterPage — the newsletter magnet (#8847 AC-6)", () => {
|
|
127
|
+
it("is rendered on every request, never ISR", () => {
|
|
128
|
+
expect(pageModule.dynamic).toBe("force-dynamic");
|
|
129
|
+
expect("revalidate" in pageModule).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("the H1, label, consent and help line are the magnet's; no upsell", async () => {
|
|
133
|
+
await renderWith(STARTER_SECTIONS);
|
|
134
|
+
|
|
135
|
+
expect(mockFlowsGet).toHaveBeenCalledWith("newsletter");
|
|
136
|
+
const headings = screen.getAllByRole("heading", { level: 1 });
|
|
137
|
+
expect(headings).toHaveLength(1);
|
|
138
|
+
expect(headings[0]).toHaveTextContent("Get the newsletter");
|
|
139
|
+
expect(document.body).toHaveTextContent("One email when there is something new.");
|
|
140
|
+
expect(screen.getByText("Email")).not.toHaveClass("sr-only");
|
|
141
|
+
const statement = screen.getByTestId("newsletter-consent-statement");
|
|
142
|
+
expect(statement).toHaveTextContent("I accept the Terms and Privacy Policy.");
|
|
143
|
+
expect(statement.querySelector("a[href='/terms']")).not.toBeNull();
|
|
144
|
+
expect(statement.querySelector("a[href='/privacy']")).not.toBeNull();
|
|
145
|
+
expect(screen.getByTestId("newsletter-help")).toHaveTextContent("No spam. Unsubscribe any time.");
|
|
146
|
+
expect(screen.getByTestId("newsletter-submit")).toHaveTextContent("Subscribe");
|
|
147
|
+
for (const upsell of ["Substack", "Pro", "ailaunchkit.ai"]) {
|
|
148
|
+
expect(document.body).not.toHaveTextContent(upsell);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
118
151
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
152
|
+
it("generateMetadata's title is the magnet's step title", async () => {
|
|
153
|
+
const metadata = await pageModule.generateMetadata({
|
|
154
|
+
params: Promise.resolve({ locale: "en" }),
|
|
155
|
+
getMdxSource: jest.fn().mockResolvedValue(source),
|
|
156
|
+
getPageSections: jest.fn().mockResolvedValue(STARTER_SECTIONS),
|
|
157
|
+
});
|
|
158
|
+
expect(String((metadata.title as { absolute?: string })?.absolute ?? metadata.title)).toContain(
|
|
159
|
+
"Get the newsletter",
|
|
123
160
|
);
|
|
124
161
|
});
|
|
125
162
|
|
|
126
|
-
it("
|
|
163
|
+
it("a signup goes through the magnet and the ending replaces the form", async () => {
|
|
164
|
+
mockSubmitStep.mockResolvedValue({
|
|
165
|
+
ok: true,
|
|
166
|
+
status: 200,
|
|
167
|
+
body: { sessionId: "s1", nextStepKey: null, completed: true },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await renderWith(STARTER_SECTIONS);
|
|
171
|
+
fireEvent.change(screen.getByTestId("newsletter-email-input"), { target: { value: "a@b.co" } });
|
|
172
|
+
fireEvent.click(screen.getByTestId("newsletter-consent-checkbox"));
|
|
173
|
+
await act(async () => {
|
|
174
|
+
fireEvent.submit(screen.getByTestId("newsletter-form"));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await waitFor(() =>
|
|
178
|
+
expect(screen.getByTestId("newsletter-success-headline")).toHaveTextContent("Check your inbox"),
|
|
179
|
+
);
|
|
180
|
+
expect(mockSubmitStep).toHaveBeenCalledWith("newsletter", {
|
|
181
|
+
stepKey: "subscribe",
|
|
182
|
+
values: { email: "a@b.co", "terms.newsletter": true },
|
|
183
|
+
locale: "en",
|
|
184
|
+
});
|
|
185
|
+
expect(leads.subscribe).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("with the hub unreadable, the seeded config renders", async () => {
|
|
189
|
+
mockFlowsGet.mockRejectedValue(new Error("hub down"));
|
|
190
|
+
|
|
191
|
+
await renderWith(STARTER_SECTIONS);
|
|
192
|
+
|
|
193
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Get the newsletter");
|
|
194
|
+
expect(screen.getByTestId("newsletter-form")).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("with capture off, ContactFallback with the _site.mdx email renders and no newsletter form", async () => {
|
|
127
198
|
mockCaptureStatus.mockResolvedValue(status(false));
|
|
128
199
|
|
|
129
|
-
|
|
200
|
+
await renderWith(STARTER_SECTIONS);
|
|
130
201
|
|
|
131
|
-
expect(screen.queryByTestId("
|
|
132
|
-
expect(
|
|
202
|
+
expect(screen.queryByTestId("newsletter-form")).toBeNull();
|
|
203
|
+
expect(document.querySelector("a[href='mailto:hi@x.co']")).not.toBeNull();
|
|
133
204
|
});
|
|
134
205
|
|
|
135
|
-
it("
|
|
136
|
-
|
|
206
|
+
it("an authored newsletter section never renders beside the slot (one signup form)", async () => {
|
|
207
|
+
await renderWith([...STARTER_SECTIONS, LEGACY_SECTIONS[1]]);
|
|
208
|
+
|
|
209
|
+
expect(screen.getAllByTestId("newsletter-form")).toHaveLength(1);
|
|
210
|
+
expect(document.querySelector("form[action='/v1/leads/subscribe']")).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
137
213
|
|
|
138
|
-
|
|
214
|
+
describe("NewsletterPage — a page with no contact-form section keeps its composition (#6730 D9)", () => {
|
|
215
|
+
it("newsletter:true + its own newsletter section → that section renders, no second form", async () => {
|
|
216
|
+
await renderWith(LEGACY_SECTIONS);
|
|
139
217
|
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
142
|
-
expect(container.querySelector("a[href='#signup']")).not.toBeNull();
|
|
218
|
+
expect(screen.getAllByTestId("newsletter-form")).toHaveLength(1);
|
|
219
|
+
expect(document.querySelector("a[href='#signup']")).not.toBeNull();
|
|
143
220
|
});
|
|
144
221
|
|
|
145
|
-
it
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(container.querySelector("a[href='#signup']")).toBeNull();
|
|
160
|
-
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
161
|
-
// The rest of the page still renders.
|
|
162
|
-
expect(container.querySelector("[data-section-id='hero']")).not.toBeNull();
|
|
163
|
-
expect(container.querySelector("[data-section-id='faq']")).not.toBeNull();
|
|
164
|
-
},
|
|
165
|
-
);
|
|
222
|
+
it("newsletter:false → the signup section and the hero CTA to it are dropped", async () => {
|
|
223
|
+
mockCaptureStatus.mockResolvedValue(status(false));
|
|
224
|
+
|
|
225
|
+
await renderWith(LEGACY_SECTIONS);
|
|
226
|
+
|
|
227
|
+
expect(screen.queryByTestId("newsletter-form")).toBeNull();
|
|
228
|
+
expect(document.querySelector("a[href='#signup']")).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("newsletter:true + no signup section → the magnet's form is appended", async () => {
|
|
232
|
+
await renderWith([{ id: "faq", type: "faq", data: { items: [] } }]);
|
|
233
|
+
|
|
234
|
+
expect(screen.getAllByTestId("newsletter-form")).toHaveLength(1);
|
|
235
|
+
});
|
|
166
236
|
});
|