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
|
@@ -110,6 +110,26 @@ function — the page under test throws instead of taking a branch. Declare
|
|
|
110
110
|
every predicate the pages call, binding the two reset ones to the same
|
|
111
111
|
`jest.fn()` when the case does not need them to disagree.
|
|
112
112
|
|
|
113
|
+
**Sign-up agreements are refused and written on the server (#8765).**
|
|
114
|
+
`lib/sign-up-agreements.ts` is a Better Auth plugin plus two database hooks
|
|
115
|
+
that `auth.ts` installs. While `isSignUpOpen()`, `POST /api/auth/sign-up/email`
|
|
116
|
+
without every sign-up agreement in its `agreements` body key answers
|
|
117
|
+
`400 AGREEMENTS_REQUIRED` before the email lookup (so the answer never depends
|
|
118
|
+
on whether the address is taken), and `503 AGREEMENTS_UNAVAILABLE` when the
|
|
119
|
+
API's `GET /v1/consent/sign-up` evidence cannot be read (60 s fresh, 10 min
|
|
120
|
+
stale-on-error, one cache entry per enabled locale). An accepted sign-up — or
|
|
121
|
+
an OAuth sign-up whose state carries the card's agreements — writes one
|
|
122
|
+
`sign_up` grant per agreement through the auth client's transaction
|
|
123
|
+
(`prismaAdapter(…, { transaction: true })`), so the `User`, its `Account` and
|
|
124
|
+
the grants commit together or not at all; this hook is the ledger's only
|
|
125
|
+
writer outside `apps/api`'s `services/consent.ts`, and it writes only rows of
|
|
126
|
+
a user it is creating. A user created without agreements (a first social
|
|
127
|
+
sign-in from `/sign-in`) has none, and the account interstitial asks them
|
|
128
|
+
(`(authed)/account/layout.tsx`, which fails closed on any read but a 2xx or an
|
|
129
|
+
older hub's 404). The carried `__Host-ailk-signup-agreements` cookie is
|
|
130
|
+
retired. Policy links on every agreement surface come from
|
|
131
|
+
`lib/consent-policies.ts`'s `consentPoliciesFor(locale)`.
|
|
132
|
+
|
|
113
133
|
## Contact page
|
|
114
134
|
|
|
115
135
|
The contact page (`apps/web/app/[locale]/contact/page.tsx`) renders the
|
|
@@ -123,10 +143,27 @@ the site's contact email, never a form that would report a message as sent
|
|
|
123
143
|
when it reached nobody. A failed status read counts as accepting, so the
|
|
124
144
|
form renders and its submit reports honestly.
|
|
125
145
|
|
|
146
|
+
**Every visible fact on the form comes from the magnet (#8847).** The page
|
|
147
|
+
reads the magnet through `lib/in-page-magnet.ts`, the in-page form shape:
|
|
148
|
+
its field step's `copy.title` is the page's H1 and `<title>` and its
|
|
149
|
+
`copy.body` the line under it (`framedSources` writes both into the first
|
|
150
|
+
`contact-form` section, and into the frontmatter title when that section
|
|
151
|
+
leads), and its `ending` is the success message (`{name}` filled from the
|
|
152
|
+
posted name). The section's own `submitLabel` and `successMessage` apply
|
|
153
|
+
only to its own fields. When the hub cannot be read the seeded
|
|
154
|
+
`contactFormFlowConfig` renders, so a fresh deploy looks the same. Under
|
|
155
|
+
the form one muted line reads "Prefer email?" (`contact.preferEmail`) and
|
|
156
|
+
the `_site.mdx` contact email, absent when the site has none. The default
|
|
157
|
+
Contact form's statement is the two-link "I accept the Terms and Privacy
|
|
158
|
+
Policy." (`CONTACT_FORM_TERMS_STATEMENT`); sign-up keeps the three-link
|
|
159
|
+
`ACCOUNT_TERMS_STATEMENT`.
|
|
160
|
+
|
|
126
161
|
The form's fields come from the workspace's **Contact form magnet** (#8661):
|
|
127
|
-
every workspace is seeded one, live at slug `contact`, and the page
|
|
128
|
-
on every request (`force-dynamic`) through the public flow read,
|
|
129
|
-
`flows.get("contact")
|
|
162
|
+
every workspace is seeded one, live at slug `contact`, and the page is rendered
|
|
163
|
+
on every request (`force-dynamic`) and reads it through the public flow read,
|
|
164
|
+
`flows.get("contact")`, shared across requests for 15 seconds per server
|
|
165
|
+
instance (#8731) — so a burst of views is one hub read, and an Admin edit shows
|
|
166
|
+
within 15 seconds. The capture-status read is shared the same way. The API resolves the workspace (the site alias
|
|
130
167
|
keyless, a spoke by its key), so the page names none. `contact-form-magnet.ts`
|
|
131
168
|
maps the config's first step onto the block's fields, terms statement and
|
|
132
169
|
submit label. The order is: a usable magnet (its first step has `name` and
|
|
@@ -135,11 +172,44 @@ declares any, else the built-in form, which is the seeded config itself. Any
|
|
|
135
172
|
failed read (absent, draft, a Free build with no flow route, a throw) is the
|
|
136
173
|
fallback, never an error. A magnet-driven post carries a hidden
|
|
137
174
|
`source: "flow:contact"`, so it is an Inbox submission of the Contact form
|
|
138
|
-
with every answer shown; a fallback post
|
|
175
|
+
with every answer shown; a fallback post is `contact-form` (#8747). The terms statement
|
|
139
176
|
links Terms, Privacy Policy and Working Agreement from a page-built
|
|
140
177
|
`policies` map, and `captureLeadAction` refuses a post without
|
|
141
|
-
`consent=true`
|
|
142
|
-
|
|
178
|
+
`consent=true` whatever its source (#8730 D2 — the hidden `source` picks the
|
|
179
|
+
stamp, never the gate) and keeps only the contact sources
|
|
180
|
+
(`CONTACT_LEAD_SOURCES`: `contact-form`, `conversational-intake`,
|
|
181
|
+
`flow:contact`). An accepted post sends `consent: true` to the API, which
|
|
182
|
+
refuses a contact lead without it (`400 consent_required`, shown as the same
|
|
183
|
+
field error) and records `payload.consent` itself.
|
|
184
|
+
|
|
185
|
+
The action writes through `src/lib/lead-write.ts` (#8747), the site's lead
|
|
186
|
+
writer: `payload.site` is `AILK_LEAD_SITE` (none when unset) and
|
|
187
|
+
`payload.page` is `contact`, both spread last so no form field overrides them,
|
|
188
|
+
and no `siteId` is sent. Before the write it runs a per-visitor limit
|
|
189
|
+
(`src/lib/rate-limit.ts`, 30 posts per 10 minutes, per instance; skipped when
|
|
190
|
+
no client address is readable), because the hub sees only this site's egress
|
|
191
|
+
address. An API that cannot be reached — a
|
|
192
|
+
thrown submit, a 502 or a 504 — renders the contact email, like the 503
|
|
193
|
+
`lead_capture_not_configured`.
|
|
194
|
+
|
|
195
|
+
**The account link (#8757 D8, D9).** When the Contact form magnet is set to
|
|
196
|
+
`invite` in Admin, the API's 201 carries `next: { completion: "invite" }` and
|
|
197
|
+
the action answers `{ kind: "success", next: { href, label } }`: the success
|
|
198
|
+
alert gains "Track this in your account" (`contact.trackInAccount`, testid
|
|
199
|
+
`contact-success-next`), built by `lib/account-invite.ts` from
|
|
200
|
+
`SIGN_IN_PROVIDER` — `site` links `/sign-in?next=<locale>/account&email=…`,
|
|
201
|
+
`hub` links `/api/hub-session/sign-in?next=/account&email=…`, `none` (or an
|
|
202
|
+
email `accountEmailHintSchema` refuses) gives no link. Anything else,
|
|
203
|
+
`checkout` included (Phase 1), is exactly `{ kind: "success" }`. The link
|
|
204
|
+
signs no one in: `/sign-in` reads one valid `email` as the card's starting
|
|
205
|
+
value (`AuthCard`'s `defaultEmail`), the spoke's
|
|
206
|
+
`/api/hub-session/sign-in` forwards it as `login_hint`, and the hub's
|
|
207
|
+
authorize route carries a valid hint only onto its no-session `/sign-in`
|
|
208
|
+
redirect, never into a code, cookie or continuation ref (a repeated
|
|
209
|
+
`login_hint` is `400 invalid_request`). `email` and `login_hint` are in the
|
|
210
|
+
telemetry scrub lists (`lib/telemetry-scrub.ts`, `packages/analytics`).
|
|
211
|
+
|
|
212
|
+
**The same link on the magnet walk and /careers (#8771).** The careers apply page's action (`/careers/<slug>/apply` on a site without the walk, #8849) reads the API's `next` the same way (`apply.actions.ts`): `invite` returns the link and `contact.trackInAccount`, anything else — or a throw after the write — exactly `{ kind: "success" }`, so an error never invites a duplicate application. The walk (`/flows/<slug>`, `FlowStepperClient`) tracks the first `mapsTo: "email"` answer it submitted; on a completing response with `next: { completion: "invite" }` it asks the server action `accountInviteLinkAction` (the builder reads the generated topology, so it never runs in the client) and shows the link inside `FlowStepper`'s finished card (its `completedAction` slot), or — for a flow with a `thankYou` block — writes one `sessionStorage` item, `ailk:account-invite`, that `/thanks/<slug>`'s `AccountInviteLink` reads once and removes (the page passes it to `renderPage` as `trailing`, so it sits inside `<main>`). Each link sits in a `data-sentry-block` element (Session Replay records `href`s); no walk URL ever carries the email.
|
|
143
213
|
|
|
144
214
|
A `contact-form` section in the `column` variant (the minimalism design's
|
|
145
215
|
Contact page) renders one centred heading, one line and the form, and the
|
|
@@ -151,12 +221,298 @@ Send disabled until the box is ticked.
|
|
|
151
221
|
The chat-style intake is not mounted by default.
|
|
152
222
|
`ConversationalIntakeWrapper.tsx` remains for a site that prefers it: it hosts
|
|
153
223
|
`@working-theory/ui`'s `ConversationalIntake` block (contract in
|
|
154
|
-
`packages/ui/src/blocks/conversational-intake/README.md`),
|
|
155
|
-
|
|
224
|
+
`packages/ui/src/blocks/conversational-intake/README.md`), asks for the terms
|
|
225
|
+
in its own last turn (#8730 D2), submits through the same
|
|
226
|
+
`captureLeadAction` with `consent=true` only when the accept pill was tapped,
|
|
227
|
+
and on a declined terms turn, Skip or a failed submit swaps to
|
|
156
228
|
`LeadCaptureForm`, prefilled with what was already captured. The canonical
|
|
157
229
|
embedding for it is behind the hero CTA's lightbox gate
|
|
158
230
|
(`packages/ui/src/sections/FormLightbox.tsx`, #4577), not a whole-page mount.
|
|
159
231
|
|
|
232
|
+
## Newsletter, unsubscribe and the footer signup
|
|
233
|
+
|
|
234
|
+
The workspace's **Newsletter magnet** (#8847) — seeded live at slug
|
|
235
|
+
`newsletter` in every workspace — is the only source of `/newsletter`,
|
|
236
|
+
`/unsubscribe` and the footer signup; none of their copy is in page code,
|
|
237
|
+
content or the message catalog. All three read it through
|
|
238
|
+
`lib/in-page-magnet.ts`'s `readServedMagnet("newsletter")`, shared for 15
|
|
239
|
+
seconds per instance like /contact's read.
|
|
240
|
+
|
|
241
|
+
- **`/newsletter`** renders on every request. The magnet's step title is the
|
|
242
|
+
H1 and `<title>`, its body the line under it, and the standard
|
|
243
|
+
`NewsletterFormInline` (visible Email label, the consent row above the
|
|
244
|
+
field, the consent field's help line, Subscribe, and the magnet's ending in
|
|
245
|
+
place of the form) goes into the page's first `contact-form` section. With
|
|
246
|
+
the hub unreadable the seeded `newsletterFlowConfig` renders; with capture
|
|
247
|
+
off (`GET /v1/leads/capture`'s `newsletter`) the slot is `ContactFallback`
|
|
248
|
+
with the `_site.mdx` email. A page with no `contact-form` section keeps its
|
|
249
|
+
pre-#8847 composition.
|
|
250
|
+
- **The signup.** `subscribeNewsletterAction` refuses a post without
|
|
251
|
+
`consent=true`, then re-reads the magnet and submits its step through
|
|
252
|
+
`flows.submitStep` — the step key, the email field and the consent key
|
|
253
|
+
(`terms.newsletter`) all from the server's read, never the post — so the
|
|
254
|
+
hub writes the lead, the `newsletter` consent row and the welcome email.
|
|
255
|
+
Without a flow route (a Free build) it posts `leads.subscribe`, as before.
|
|
256
|
+
It lives in `lib/newsletter.actions.ts`, not under `app/[locale]/newsletter/`,
|
|
257
|
+
because the footer imports it and a scaffold with the `newsletter` surface
|
|
258
|
+
off drops that route directory.
|
|
259
|
+
- **`/unsubscribe`** takes its title, line, button and done message from the
|
|
260
|
+
magnet's `unsubscribe` block (strings only across the island edge); the
|
|
261
|
+
already, invalid and unavailable lines stay in the `unsubscribe` catalog.
|
|
262
|
+
- **The footer** fills `Footer`'s `subscribe` slot with the compact form
|
|
263
|
+
(stacked, `sr-only` label) only when the magnet is served and capture is
|
|
264
|
+
on; the built-in config never fills it.
|
|
265
|
+
|
|
266
|
+
## Careers
|
|
267
|
+
|
|
268
|
+
Three linked pages, each built from the `content/<locale>/JobPosting/*.mdx`
|
|
269
|
+
records (#8849):
|
|
270
|
+
|
|
271
|
+
- **`/careers`** renders in sections mode, the /alternatives pattern: one
|
|
272
|
+
generated `items` section first, a card per posting (newest `publishedAt`
|
|
273
|
+
first), then the seed's declared sections, so the card grid and the
|
|
274
|
+
`CollectionPage`'s `ItemList` (`ListItem`s with `position` and `url`) agree.
|
|
275
|
+
The section's `filter` gives a search box and employment-type and location
|
|
276
|
+
selects; the facets read the postings' first-class `employmentType` and
|
|
277
|
+
`location` frontmatter, never `schema.mainEntity`. Every posting stays in the
|
|
278
|
+
server HTML. Every visible string is a `careers` message.
|
|
279
|
+
- **`/careers/<slug>`** keeps its chrome and ends with an Apply button
|
|
280
|
+
(`job-apply-cta`, `Button` size `big`) to `/careers/<slug>/apply`; its
|
|
281
|
+
`ApplyAction` targets the same URL. It makes no hub read.
|
|
282
|
+
- **`/careers/<slug>/apply`** is the `signal-collectors` walk where the module
|
|
283
|
+
is present (§ Magnet pages), else the Free apply page,
|
|
284
|
+
`careers/[slug]/[action]/page.tsx`: the posting's title over the one-step
|
|
285
|
+
`LeadCaptureForm` bound to `apply.actions.ts` with the slug as `job`,
|
|
286
|
+
`noindex, follow` and full-screen. Next.js serves the static `apply`
|
|
287
|
+
segment first, the `[slug]/[detail]` idiom; every other action 404s. Lead
|
|
288
|
+
capture is never-gate, so every tier takes an application. Both apply pages
|
|
289
|
+
read the posting's title through `lib/posting-title.ts`.
|
|
290
|
+
|
|
291
|
+
## Meeting pages
|
|
292
|
+
|
|
293
|
+
The scheduling module's booking pages (#8843) read every meeting fact from
|
|
294
|
+
the workspace — no content file, env var or deploy sits between a Meetings or
|
|
295
|
+
Cal.com edit and the page.
|
|
296
|
+
|
|
297
|
+
- **One builder.** `lib/meetings.tsx` reads `GET /v1/schedule/meetings`
|
|
298
|
+
through `schedule.meetings()`, re-parses the body with `MeetingListSchema`
|
|
299
|
+
and shares the result per server instance for 15 seconds (concurrent and
|
|
300
|
+
failed reads too, the contact page's #8731 shape). Any failure is
|
|
301
|
+
`{ ok: false }`. It builds the index cards (`meetingCardsSection`: `items`
|
|
302
|
+
tiles, two columns, `gapAbove: "tight"`, one card per bookable meeting with
|
|
303
|
+
the hub's title and description and a "Book <n> min" button) and the
|
|
304
|
+
meeting page's booker (`meetingPageSections`: `scheduler-embed`, provider
|
|
305
|
+
`cal`, month view). Every visible string is a `schedule` message.
|
|
306
|
+
- **`/meet` and `/private`** both return `MeetingIndex`
|
|
307
|
+
(`app/[locale]/meet/meeting-index.tsx` — beside the route, because it
|
|
308
|
+
renders the paid `BookingProfileHeader`, which `architecture.yaml` admits
|
|
309
|
+
only there): the back link alone (`Nav` `bar="bare"` `bareMark={false}`),
|
|
310
|
+
the header's avatar and one H1, then the cards of that visibility. One
|
|
311
|
+
meeting redirects to `/meet/<slug>`; none, or a failed read, shows the
|
|
312
|
+
site's contact email — never a 404 or a 500. `/private` is
|
|
313
|
+
`noindex, nofollow`, has no seed and no nav entry: private means unlisted,
|
|
314
|
+
not access-controlled.
|
|
315
|
+
- **`/meet/<slug>`** is one centred H1 ("Schedule a <title> meeting"),
|
|
316
|
+
Cal.com's inline embed from the hub's Cal.com link, and "← All meeting
|
|
317
|
+
types". A Live meeting that cannot be booked, or a failed read for a valid
|
|
318
|
+
slug, shows the contact email; an unknown slug or a dotted one is a 404. A
|
|
319
|
+
`BookCall/<slug>.mdx` seed gives only the URL, the JSON-LD inputs and the
|
|
320
|
+
fallback title — its sections are never rendered here.
|
|
321
|
+
- **Chrome.** `lib/modules.generated.ts`'s `SCHEDULING_PAGES` (written by
|
|
322
|
+
create-ailk from `apps/web/package.json`, never copied by `update`) and
|
|
323
|
+
`lib/auth-routes.ts`'s `isBookingPath` make the locale layout drop the
|
|
324
|
+
marketing shell on `/meet`, `/meet/<slug>` and `/private`; `/meet/confirmed`
|
|
325
|
+
and a site without the module keep it. The consent banner stays.
|
|
326
|
+
- **The CSP (D12).** `security-headers.config.ts` exports `SCHEDULING_CSP`
|
|
327
|
+
(Cal.com's app origin in `script-src` and `connect-src`; `frame-src`
|
|
328
|
+
`'self'` and the same origin) and a frozen `SCHEDULING_SECURITY_HEADERS`.
|
|
329
|
+
`middleware.ts` applies it instead of `DEFAULT_SECURITY_HEADERS` only when
|
|
330
|
+
`SCHEDULING_PAGES && isBookingPath(pathname, enabledLocales)`; sign-in,
|
|
331
|
+
account, checkout, `/meet/confirmed` and every site without the module keep
|
|
332
|
+
the default policy. `architecture.yaml`'s `cal\.com` rule excludes the
|
|
333
|
+
config, the middleware and their two tests for this reason.
|
|
334
|
+
|
|
335
|
+
## Magnet pages
|
|
336
|
+
|
|
337
|
+
Every multi-step magnet is a page pair (#8840), rendered by one module,
|
|
338
|
+
`app/[locale]/flows/[slug]/magnet-page.tsx`:
|
|
339
|
+
|
|
340
|
+
- **`/<magnet>`** — the indexable landing, inside the site's Nav and Footer: a
|
|
341
|
+
centred hero of the flow record's `landing` copy (title, line, muted meta
|
|
342
|
+
line, button to `/<magnet>/start`; absent keys fall back to step 1's title
|
|
343
|
+
and the `flow` messages) and the `Assessment` page type's `WebApplication`
|
|
344
|
+
JSON-LD.
|
|
345
|
+
- **`/<magnet>/start`** — the walk, `noindex, follow`, full-screen with the
|
|
346
|
+
back arrow alone (`NavBackLink`) above step 1. `lib/magnet-routes.ts`'s
|
|
347
|
+
`isMagnetWalkPath` is what the locale layout drops the Nav and Footer on:
|
|
348
|
+
any path ending in `start`, plus `/careers/apply` and
|
|
349
|
+
`/careers/<posting>/apply`. The cookie consent banner stays.
|
|
350
|
+
- **The ending.** A flow with an `ending` ends on it, never on `thankYou`:
|
|
351
|
+
`FlowStepperClient` picks the outcome from the walk's answers
|
|
352
|
+
(`selectFlowEndingOutcome`) — a screen in the finished card with `{name}`
|
|
353
|
+
filled, or a redirect to a site path — and re-checks every destination
|
|
354
|
+
against `flowSitePathSchema` before it renders or navigates.
|
|
355
|
+
- **The hub is down.** Both pages read the flow through one `flows.get`,
|
|
356
|
+
shared per instance for 15 seconds per slug; a refused connection or a 5xx
|
|
357
|
+
shows `ContactFallback` with the site's contact email (both pages
|
|
358
|
+
`noindex`), and a 404 is `notFound()`. `/careers/apply` exists apart from
|
|
359
|
+
its flow, so it passes `renderMagnetStart` a `title`: a 404, a refused
|
|
360
|
+
connection or a 5xx all render the contact email under that `<h1>`
|
|
361
|
+
(#9219).
|
|
362
|
+
- **The pairs.** `/flows/<slug>` is the canonical pair. `/careers/apply` and
|
|
363
|
+
`/careers/<posting>/apply` walk the Job application magnet (the second
|
|
364
|
+
titles step 1 with the posting and sends its slug as the hidden `job`).
|
|
365
|
+
Any other pair is written by `create-ailk add-route magnet --path <path>
|
|
366
|
+
--flow <slug>`: two short pages carrying the `Standard magnet page
|
|
367
|
+
(#8840)` marker, which `lib/__tests__/structured-data.test.tsx` exempts
|
|
368
|
+
from its `renderPage` check. A site adds a magnet by binding a pair to a
|
|
369
|
+
slug, never by copying stepper code.
|
|
370
|
+
- **The recommender's landing (#9071).** A recommending flow with an offer
|
|
371
|
+
carrying `lookupKey` (an Offerings recommender, such as the standard
|
|
372
|
+
`recommender` magnet the Admin's Offerings page opens) never renders its
|
|
373
|
+
results page: the results permalink
|
|
374
|
+
(`flows/[slug]/results/[payload]/page.tsx`) redirects a completed walk to
|
|
375
|
+
`/<locale>/pricing?plan=<plan>` while the first fit's lookup key is in the
|
|
376
|
+
current shown set, else to `/<locale>/pricing` with no badge (no fit, a key
|
|
377
|
+
no longer shown, any failed read, or no answer within 5 seconds). The
|
|
378
|
+
shown-set read is `offerings.shown()`, `/pricing`'s own, shared per
|
|
379
|
+
instance for 15 seconds; `?plan=` carries the key's `{plan}` part, the only
|
|
380
|
+
value `/pricing` reads. A flow with no such offer, or whose first fit
|
|
381
|
+
carries none, renders the results page as before, with no read.
|
|
382
|
+
- **The deliverable pages (#8848).** A flow whose served config carries a
|
|
383
|
+
`deliverable` block (a `file`, `video`, `page` or `external` payload, gated
|
|
384
|
+
or not) has one route, `/reports/<slug>`, rendered by
|
|
385
|
+
`app/[locale]/reports/deliverable-page.tsx`; both halves of every other pair
|
|
386
|
+
redirect a deliverable flow there. Gated: the page is a centred hero of the
|
|
387
|
+
block's copy with the back arrow to `/reports` and a button to the walk at
|
|
388
|
+
`/reports/<slug>/start`, and the walk's finished card shows the deliverable
|
|
389
|
+
after the ending screen — the reader link `/reports/<slug>?g=<grant>` (in a
|
|
390
|
+
`data-sentry-block` element), an inline player, a site link or a link out;
|
|
391
|
+
a redirect ending never runs when the response carries one, and a file walk
|
|
392
|
+
that minted no grant says the link is in the email. Ungated: the reader over
|
|
393
|
+
the site's own `/reports/<slug>/file` route handler (it reads the hub's
|
|
394
|
+
`GET /v1/deliverables/open/:slug` with the spoke key and the visitor
|
|
395
|
+
forward, 30 reads per visitor per 10 minutes — skipped when no client
|
|
396
|
+
address is readable — the five file headers passed through), a player, a redirect to the site page, or a button out. Every
|
|
397
|
+
page under `/reports/<slug>` is chrome-free (`isMagnetWalkPath`); `/reports`
|
|
398
|
+
keeps its chrome. The reader's Share is built from the page URL, never the
|
|
399
|
+
bytes or `?g=`. A hub read that fails falls back to the slug's `ReportPage`
|
|
400
|
+
content. With none, #8848 D6 holds for a configured hub: a 5xx or a refused
|
|
401
|
+
connection shows the contact email (`noindex`). A site with no hub to ask —
|
|
402
|
+
the read answers 503 `flows_not_configured`, or no API origin
|
|
403
|
+
(`AILK_API_URL`, `NEXT_PUBLIC_AILK_API_URL`, `NEXT_PUBLIC_API_URL`) is set —
|
|
404
|
+
reads `not-configured` (set once in `readMagnet`), and the slug 404s, as
|
|
405
|
+
does its `/start` (#9219). `/downloads` and `/downloads/<slug>` are
|
|
406
|
+
permanent redirects into `/reports` (signal-collectors files).
|
|
407
|
+
|
|
408
|
+
## Pricing page
|
|
409
|
+
|
|
410
|
+
The pricing page (`apps/web/app/[locale]/pricing/page.tsx`) renders the
|
|
411
|
+
workspace's Offerings shown set from Stripe (#8927). Before rendering it
|
|
412
|
+
reads `GET /v1/content/shown-offerings` through `offerings.shown()` from
|
|
413
|
+
`@working-theory/api-client`, renders on every request (`force-dynamic`),
|
|
414
|
+
and shares the read across requests for 15 seconds per server instance, the
|
|
415
|
+
contact page's window and reason: a product switched off disappears within
|
|
416
|
+
15 seconds, with no deploy. The API resolves the workspace (a spoke by its
|
|
417
|
+
key), so the page names none. It renders one of three pages:
|
|
418
|
+
|
|
419
|
+
- **Unconnected** — the route answers `unconnected`, or the deployment runs
|
|
420
|
+
in production with none of the API origin variables `getApiBase()` reads:
|
|
421
|
+
today's MDX page through `renderPage`, byte for byte, `?plan=` matching
|
|
422
|
+
authored plan keys as #8842 made it.
|
|
423
|
+
- **A current shown set** — `lib/offerings-pricing.ts` maps it to a plans
|
|
424
|
+
fragment and an add-ons fragment. The first `pricing` section that is not
|
|
425
|
+
`addons` becomes the Stripe plans, and the first `addons` one the Stripe
|
|
426
|
+
add-ons, each keeping only its authored section copy (eyebrow, headline,
|
|
427
|
+
description and layout knobs), never an authored plan or price. Every other
|
|
428
|
+
`pricing` section is removed; a fragment with no section to replace goes
|
|
429
|
+
after a leading hero. `?plan=` moves the recommendation only for a
|
|
430
|
+
`{plan}`-grammar value (`LOOKUP_KEY_PLAN_PATTERN`), so `?plan=Growth` keeps
|
|
431
|
+
the workspace's own. Copy comes from the `pricingOfferings` namespace.
|
|
432
|
+
- **Anything else** — an expired or empty shown set, `empty`, a non-2xx, an
|
|
433
|
+
unknown body or a throw: every `pricing` section gives way to one
|
|
434
|
+
`contact-form` section holding `ContactFallback` with the site's contact
|
|
435
|
+
email (`readSiteContactEmail()`), in the `contact` namespace's copy. Never
|
|
436
|
+
an error page, and no `Offer` is published.
|
|
437
|
+
|
|
438
|
+
Stripe text reaches the page only as React text and JSON-LD only through
|
|
439
|
+
`renderPage`'s serializer; the title and meta stay the MDX page's.
|
|
440
|
+
|
|
441
|
+
Every Stripe card links `/api/offerings/checkout/<lookup_key>`, which
|
|
442
|
+
`app/api/offerings/checkout/[lookupKey]/route.ts` serves (#9051). Only a
|
|
443
|
+
buyer's page load starts anything: a request that is not a top-level
|
|
444
|
+
navigation (`Sec-Fetch-Mode: navigate`, `Sec-Fetch-Dest: document`), a
|
|
445
|
+
`HEAD`, a prefetch, an RSC fetch or a cross-site load is refused before any
|
|
446
|
+
call. A key `parseLookupKey` refuses, a site without the `commerce` claim, and
|
|
447
|
+
production with no API origin are refused locally too. The route then reads
|
|
448
|
+
the shown set, shared per instance for 15 seconds like the page's own read,
|
|
449
|
+
and goes on only when the site is connected and that set shows the key, so an
|
|
450
|
+
unconnected site never reaches the API's env path. Each visitor address gets 5
|
|
451
|
+
starts per 10 minutes per instance. The route POSTs `/v1/checkout/sessions`
|
|
452
|
+
through `checkout.createSession` with the visitor's `x-forwarded-for`, success
|
|
453
|
+
URL `/purchase/success?session_id={CHECKOUT_SESSION_ID}` and cancel URL
|
|
454
|
+
`/pricing`, both on the origin of `resolveBaseUrl()` and never the request's
|
|
455
|
+
host, and answers a `303` to the `https:` Checkout URL the API returns. Every
|
|
456
|
+
refusal is a `303` to `/pricing` with no body, `Cache-Control: no-store` and
|
|
457
|
+
one `[offerings-checkout] refused reason=<reason>` warning; no response or
|
|
458
|
+
log line carries the key. It sends `OSS_API_INTERNAL_SECRET` as
|
|
459
|
+
`x-internal-auth` only from a site that is not a spoke and only to an
|
|
460
|
+
`https:` or loopback API, so the hub's own `apps/web` must hold the API's
|
|
461
|
+
value when the API sets it. A spoke never holds that secret: it sells only
|
|
462
|
+
when its hub lists the spoke's origin in `AILK_TRUSTED_ORIGINS` and does not
|
|
463
|
+
set `OSS_API_INTERNAL_SECRET`, and otherwise every spoke buy returns to
|
|
464
|
+
`/pricing`.
|
|
465
|
+
|
|
466
|
+
An existing scaffold adopts the new page by bringing
|
|
467
|
+
`lib/offerings-pricing.ts`, `app/api/offerings/checkout/[lookupKey]/route.ts`
|
|
468
|
+
(it sits under `app/api/`, outside the route registry `sync-routes` copies),
|
|
469
|
+
the `pricingOfferings` namespace and an
|
|
470
|
+
`@working-theory/api-client` release that exports `offerings` with it, and
|
|
471
|
+
needs an API (its own `apps/api`, or a spoke's hub) that serves
|
|
472
|
+
`GET /v1/content/shown-offerings` (#8924): an older API answers that path
|
|
473
|
+
non-2xx, which renders the contact email in place of the authored pricing.
|
|
474
|
+
|
|
475
|
+
## Spoke checkout
|
|
476
|
+
|
|
477
|
+
**A spoke sells through the Offerings buy route, and only through it.**
|
|
478
|
+
`app/api/offerings/checkout/[lookupKey]/route.ts` (§ Pricing page) is the one
|
|
479
|
+
selling path on every site: the seller's Stripe account is held once per
|
|
480
|
+
Organization on the hub, and the hub authorizes each buy against the
|
|
481
|
+
workspace's shown set. The spoke's legacy env-key checkout — its own buy route
|
|
482
|
+
(`app/api/checkout/[offering]/route.ts`), its Stripe webhook route
|
|
483
|
+
(`app/api/webhooks/stripe/route.ts`), `lib/checkout.ts` (the fulfilment) and
|
|
484
|
+
the generated checkout e2e workflow — is retired (#9142). `create-ailk
|
|
485
|
+
sync-routes --apply` removes them from an existing spoke when they are still
|
|
486
|
+
the copies create-ailk wrote, and names any it leaves.
|
|
487
|
+
|
|
488
|
+
What a spoke that carries `commerce` still gets (#8748), kept current by
|
|
489
|
+
`create-ailk update`; a hub, or a spoke without `commerce`, never gets them:
|
|
490
|
+
|
|
491
|
+
- **The two return pages.** `app/[locale]/checkout/thank-you/page.tsx` and
|
|
492
|
+
`app/[locale]/checkout/unavailable/page.tsx`. Both call `notFound()` unless
|
|
493
|
+
`lib/topology.generated.ts`'s `TOPOLOGY` is `"spoke"`, so this monorepo's
|
|
494
|
+
hub-shaped `apps/web` serves neither.
|
|
495
|
+
- **One data file.** `lib/checkout-config.ts` is the site's stamp,
|
|
496
|
+
`checkoutConfig.site`, and nothing else: no import and no code.
|
|
497
|
+
`create-ailk update` never rewrites it.
|
|
498
|
+
- **One package reader.** The thank-you page may import
|
|
499
|
+
`@working-theory/spoke-checkout` for `readPaidCheckoutEmail` alone
|
|
500
|
+
(`architecture.yaml`, #8757 D13): it reads `session_id`, and for a paid
|
|
501
|
+
session of this site under 24 hours old shows "Open your account" with the
|
|
502
|
+
buyer's email (`accountInviteHref`), else the same link with no email. Each
|
|
503
|
+
visitor gets 10 reads per 10 minutes (`src/lib/rate-limit.ts`); the page
|
|
504
|
+
sends no referrer, renders dynamically and never touches a cookie. The read
|
|
505
|
+
admits the spoke's `STRIPE_SECRET_KEY` from `process.env`; that key never
|
|
506
|
+
sells.
|
|
507
|
+
- **The billing action (a spoke that signs in through its hub).** create-ailk
|
|
508
|
+
renders `app/[locale]/(authed)/account/billing/spoke-billing.action.ts`,
|
|
509
|
+
which builds the package's billing reads from `checkoutConfig.site` and
|
|
510
|
+
`resolveBaseUrl()`; the spoke's own `architecture.yaml` admits it as the
|
|
511
|
+
package's other importer.
|
|
512
|
+
- **No `Idempotency-Key`.** No `apps/web` file sends one
|
|
513
|
+
(`tests/architecture/idempotency-key-producers.test.ts`): the fulfilment that
|
|
514
|
+
did is retired with the legacy checkout.
|
|
515
|
+
|
|
160
516
|
## The same-origin relay (`/v1/*`)
|
|
161
517
|
|
|
162
518
|
A visitor's browser never calls the hub. Lead capture, newsletter signup, callback, scheduling and the commerce invoice call post to this site's own `/v1/...` address, and `app/v1/[...path]/route.ts` hands each request to `lib/relay.ts` (`handleRelay`), which forwards it server-side through `@working-theory/api-client`'s `relayToApi` with the site's own `AILK_SPOKE_KEY` when it has one (#7644). The browser never holds the key, so a hub can refuse unknown callers (#7633) without taking a site's forms down.
|
|
@@ -183,4 +539,32 @@ A visitor's browser never calls the hub. Lead capture, newsletter signup, callba
|
|
|
183
539
|
- Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
|
|
184
540
|
- Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
|
|
185
541
|
|
|
186
|
-
Last updated: 2026-10-
|
|
542
|
+
Last updated: 2026-10-19 — § Magnet pages: `/careers/apply` renders the contact email under its own `<h1>` when its flow is absent or the hub cannot answer; `readMagnet`'s `not-configured` kind 404s an unknown `/reports/<slug>` (and its `/start`) on a site with no hub to ask, while a configured hub that is down keeps #8848 D6's contact email (#9219) — verified against `app/[locale]/flows/[slug]/magnet-page.tsx`, `app/[locale]/careers/apply/page.tsx`, `app/[locale]/reports/deliverable-page.tsx` and `app/[locale]/reports/[slug]/{page,start/page}.tsx`.
|
|
543
|
+
|
|
544
|
+
Prior: 2026-10-18 — § Spoke checkout: the Offerings buy route is the one selling path, and the legacy env-key buy route, webhook route, `lib/checkout.ts` and checkout e2e workflow are retired (#9142) — verified against `app/api/offerings/checkout/[lookupKey]/route.ts`, `app/[locale]/checkout/*/page.tsx`, `lib/checkout-config.ts` and create-ailk's `spoke-checkout.ts` and `sync-routes.ts`.
|
|
545
|
+
|
|
546
|
+
Prior: 2026-10-18 — § Magnet pages: the recommender's landing on `/pricing?plan=` (#9071) — verified against `app/[locale]/flows/[slug]/results/[payload]/page.tsx` and its `landing.test.tsx`.
|
|
547
|
+
|
|
548
|
+
Prior: 2026-10-18 — § Magnet pages: the deliverable pages at `/reports/<slug>`, their ending, the file proxy and the `/downloads` redirects (#8848) — verified against `app/[locale]/reports/{deliverable-page.tsx,[slug]/page.tsx,[slug]/start/page.tsx,[slug]/file/route.ts}`, `app/[locale]/flows/[slug]/{magnet-page,FlowStepperClient}.tsx`, `app/[locale]/downloads/` and `lib/magnet-routes.ts`.
|
|
549
|
+
|
|
550
|
+
Prior: 2026-10-18 — § Contact page and § Newsletter, unsubscribe and the footer signup: the in-page form shape — every visible fact from the Contact form and Newsletter magnets (#8847) — verified against `lib/in-page-magnet.ts`, `app/[locale]/{contact,newsletter,unsubscribe}/page.tsx`, `lib/newsletter.actions.ts` and `app/[locale]/layout.tsx`.
|
|
551
|
+
|
|
552
|
+
Prior: 2026-10-18 — § Careers: the searchable /careers, the posting's Apply button and the Free apply page (#8849) — verified against `app/[locale]/careers/{page,[slug]/page,[slug]/[action]/page}.tsx` and `lib/posting-title.ts`; § Pricing page: the Offerings buy route `app/api/offerings/checkout/[lookupKey]/route.ts` starts the hosted Checkout for a shown key and returns every refusal to `/pricing`, and the spoke precondition (#9051) — verified against the route, its suite and `lib/offerings-pricing.ts`.
|
|
553
|
+
|
|
554
|
+
Prior: 2026-10-18 — § Meeting pages: the booking pages carry the Cal.com CSP (`SCHEDULING_CSP`, applied by `middleware.ts` on booking paths only) and `scheduler-embed` forwards `gapAbove` (#8843 D12, D13) — verified against `security-headers.config.ts`, `middleware.ts` and `packages/ui/src/sections/SchedulerEmbed.tsx`.
|
|
555
|
+
|
|
556
|
+
Prior: 2026-10-18 — § Meeting pages: the booking pages read the workspace's meetings through one builder, the chrome-free booking branch and its module flag (#8843) — verified against `lib/meetings.tsx`, `app/[locale]/meet/`, `app/[locale]/private/page.tsx`, `lib/auth-routes.ts` and `app/[locale]/layout.tsx`.
|
|
557
|
+
|
|
558
|
+
Prior: 2026-10-18 — § Magnet pages: the standard magnet page pair, its walk predicate, ending, hub-down fallback and `add-route magnet` (#8840) — verified against `app/[locale]/flows/[slug]/magnet-page.tsx`, `FlowStepperClient.tsx`, `lib/magnet-routes.ts`, `app/[locale]/layout.tsx` and `app/[locale]/careers/{apply,[slug]/apply}/page.tsx`.
|
|
559
|
+
|
|
560
|
+
Prior: 2026-10-18 — § Pricing page: `/pricing` renders the Offerings shown set, the contact email when there is none to sell, and today's page for an unconnected site (#8927) — verified against `app/[locale]/pricing/page.tsx` and `lib/offerings-pricing.ts`.
|
|
561
|
+
|
|
562
|
+
Prior: 2026-10-16 — § Spoke checkout: the purchase lead's `termsAccepted` key, the stamp of the terms the buyer accepted, from which the hub writes the checkout consent grant (#8778) — verified against `lib/checkout.ts` and `packages/spoke-checkout/src/webhook.ts`.
|
|
563
|
+
|
|
564
|
+
Prior: 2026-10-16 — § Spoke checkout: the thank-you page reads the paid session's email for "Open your account" (#8757 D13, #8800) — verified against `app/[locale]/checkout/thank-you/page.tsx` and `packages/spoke-checkout/src/paid-email.ts`.
|
|
565
|
+
|
|
566
|
+
Prior: 2026-10-15 — § The `(auth)` route group: sign-up agreements are refused and written on the server, the carried cookie is retired, and the account interstitial asks a user with no current grant (#8765) — verified against `lib/sign-up-agreements.ts`, `auth.ts`, `lib/consent-policies.ts` and `app/[locale]/(authed)/account/layout.tsx`.
|
|
567
|
+
|
|
568
|
+
Prior: 2026-10-15 — § Spoke checkout: the standard spoke checkout files, the one data file, the 404 and the fulfilment's one-key rule (#8748) — verified against `app/api/checkout/[offering]/route.ts`, `app/api/webhooks/stripe/route.ts`, `app/[locale]/checkout/*/page.tsx`, `lib/checkout.ts` and `lib/checkout-config.ts`.
|
|
569
|
+
|
|
570
|
+
Prior: 2026-10-15 — § Contact page: the account link an `invite` magnet adds, and the sign-in email hint (#8757).
|
|
@@ -24,10 +24,14 @@ const catalog = msgs as unknown as Record<string, Record<string, string>>;
|
|
|
24
24
|
// getTranslations({ namespace, locale }) — renderPage (post-migration)
|
|
25
25
|
export const getTranslations = async (
|
|
26
26
|
arg: string | { namespace?: string; locale?: string },
|
|
27
|
-
): Promise<(key: string) => string> => {
|
|
27
|
+
): Promise<((key: string) => string) & { has: (key: string) => boolean }> => {
|
|
28
28
|
const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
|
|
29
29
|
const messages = catalog[ns];
|
|
30
|
-
|
|
30
|
+
const t = (key: string): string => messages?.[key] ?? `${ns}.${key}`;
|
|
31
|
+
// next-intl's `t.has` — an optional catalog key (#8790's prospect line) is
|
|
32
|
+
// present only when the site added it.
|
|
33
|
+
t.has = (key: string): boolean => messages?.[key] !== undefined;
|
|
34
|
+
return t;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
export const getRequestConfig = (fn: unknown): unknown => fn;
|
|
@@ -141,9 +141,9 @@ describe("the (auth) layout", () => {
|
|
|
141
141
|
expect(document.querySelector(".ailk-footer")).not.toBeInTheDocument();
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
-
it("renders
|
|
144
|
+
it("renders no brand row (the mark is in each card, #8790 D3) and the slim footer's three links", async () => {
|
|
145
145
|
await renderLayout();
|
|
146
|
-
expect(screen.
|
|
146
|
+
expect(screen.queryByTestId("auth-shell-brand")).not.toBeInTheDocument();
|
|
147
147
|
const footer = screen.getByTestId("auth-shell-footer");
|
|
148
148
|
expect(footer.querySelectorAll("a")).toHaveLength(3);
|
|
149
149
|
expect(
|
|
@@ -193,9 +193,10 @@ describe("the moved sign-in card", () => {
|
|
|
193
193
|
expect(screen.queryByTestId("auth-forgot")).not.toBeInTheDocument();
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
it("renders
|
|
196
|
+
it("renders the site's mark inside the card, not in the shell (#8790 D3)", async () => {
|
|
197
197
|
await renderPage(SignInPage);
|
|
198
|
-
expect(screen.
|
|
198
|
+
expect(screen.getByTestId("auth-card-logo")).toBeInTheDocument();
|
|
199
|
+
expect(screen.queryByTestId("auth-shell-brand")).not.toBeInTheDocument();
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
|
|
@@ -32,12 +32,14 @@ import {
|
|
|
32
32
|
type AuthSubmitData,
|
|
33
33
|
} from "@working-theory/ui/blocks";
|
|
34
34
|
import { createAuthClient } from "better-auth/react";
|
|
35
|
-
import type { ReactElement } from "react";
|
|
35
|
+
import type { ReactElement, ReactNode } from "react";
|
|
36
36
|
import { useState } from "react";
|
|
37
37
|
|
|
38
38
|
const authClient = createAuthClient();
|
|
39
39
|
|
|
40
40
|
export interface ForgotPasswordCardProps {
|
|
41
|
+
/** The site's mark, centred in the card at 48px (#8790 D3). */
|
|
42
|
+
logo?: ReactNode;
|
|
41
43
|
/** Absolute same-origin URL of `/reset-password`, resolved server-side. */
|
|
42
44
|
resetUrl: string;
|
|
43
45
|
/** Href for the card footer's "Back to sign in". */
|
|
@@ -53,6 +55,7 @@ export function ForgotPasswordCard({
|
|
|
53
55
|
signInHref,
|
|
54
56
|
labels,
|
|
55
57
|
errorLabels,
|
|
58
|
+
logo,
|
|
56
59
|
}: ForgotPasswordCardProps): ReactElement {
|
|
57
60
|
const [sent, setSent] = useState(false);
|
|
58
61
|
const [sentTo, setSentTo] = useState("");
|
|
@@ -85,6 +88,7 @@ export function ForgotPasswordCard({
|
|
|
85
88
|
return (
|
|
86
89
|
<div className="flex flex-col gap-4" data-testid="forgot-password-page">
|
|
87
90
|
<AuthCard
|
|
91
|
+
{...(logo !== undefined ? { logo } : {})}
|
|
88
92
|
intent={sent ? "sent" : "forgot"}
|
|
89
93
|
signInHref={signInHref}
|
|
90
94
|
labels={labels}
|
|
@@ -40,6 +40,7 @@ import type { ReactElement } from "react";
|
|
|
40
40
|
import { resetSendConfigured } from "../../../../lib/auth-env";
|
|
41
41
|
|
|
42
42
|
import { ForgotPasswordCard } from "./forgot-password-card";
|
|
43
|
+
import { siteLogo } from "../../../../lib/site-logo";
|
|
43
44
|
|
|
44
45
|
interface Props {
|
|
45
46
|
params: Promise<{ locale: string }>;
|
|
@@ -100,6 +101,7 @@ export default async function ForgotPasswordPage({
|
|
|
100
101
|
|
|
101
102
|
return (
|
|
102
103
|
<ForgotPasswordCard
|
|
104
|
+
logo={siteLogo()}
|
|
103
105
|
resetUrl={resetUrl}
|
|
104
106
|
signInHref={signInHref}
|
|
105
107
|
labels={labels}
|
|
@@ -24,7 +24,6 @@ import { headers } from "next/headers";
|
|
|
24
24
|
import { getTranslations } from "next-intl/server";
|
|
25
25
|
import type { ReactElement, ReactNode } from "react";
|
|
26
26
|
|
|
27
|
-
import { siteLogo } from "../../../lib/site-logo";
|
|
28
27
|
|
|
29
28
|
interface Props {
|
|
30
29
|
children: ReactNode;
|
|
@@ -51,19 +50,9 @@ export default async function AuthLayout({
|
|
|
51
50
|
|
|
52
51
|
return (
|
|
53
52
|
<AuthShell
|
|
54
|
-
brand
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* Nav: `siteLogo()` emits an <img> with a height ATTRIBUTE and no
|
|
58
|
-
* width, and an SVG with no intrinsic size then stretches to the
|
|
59
|
-
* container — it rendered 48 x 350 here before the clamp. The shell
|
|
60
|
-
* owns the row's geometry; the site owns its mark's size.
|
|
61
|
-
*/
|
|
62
|
-
<div className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain">
|
|
63
|
-
{siteLogo()}
|
|
64
|
-
</div>
|
|
65
|
-
}
|
|
66
|
-
brandHref={localeHref(locale, "/")}
|
|
53
|
+
// No brand row (#8790 D3): every auth card now carries the site's mark
|
|
54
|
+
// centred at 48px (`AuthCard`'s `logo` slot), so the shell's top-start
|
|
55
|
+
// corner is free for a page's own affordance (the sign-in's back link).
|
|
67
56
|
footerLinks={footerLinks}
|
|
68
57
|
languageSwitcher={
|
|
69
58
|
<LocaleSwitcherNav
|
|
@@ -39,6 +39,7 @@ import type { ReactElement } from "react";
|
|
|
39
39
|
import { resetSendConfigured } from "../../../../lib/auth-env";
|
|
40
40
|
|
|
41
41
|
import { ResetPasswordCard } from "./reset-password-card";
|
|
42
|
+
import { siteLogo } from "../../../../lib/site-logo";
|
|
42
43
|
|
|
43
44
|
interface Props {
|
|
44
45
|
params: Promise<{ locale: string }>;
|
|
@@ -122,6 +123,7 @@ export default async function ResetPasswordPage({
|
|
|
122
123
|
|
|
123
124
|
return (
|
|
124
125
|
<ResetPasswordCard
|
|
126
|
+
logo={siteLogo()}
|
|
125
127
|
token={token}
|
|
126
128
|
signInHref={signInHref}
|
|
127
129
|
labels={labels}
|
|
@@ -23,12 +23,14 @@ import {
|
|
|
23
23
|
type AuthSubmitData,
|
|
24
24
|
} from "@working-theory/ui/blocks";
|
|
25
25
|
import { createAuthClient } from "better-auth/react";
|
|
26
|
-
import type { ReactElement } from "react";
|
|
26
|
+
import type { ReactElement, ReactNode } from "react";
|
|
27
27
|
import { useState } from "react";
|
|
28
28
|
|
|
29
29
|
const authClient = createAuthClient();
|
|
30
30
|
|
|
31
31
|
export interface ResetPasswordCardProps {
|
|
32
|
+
/** The site's mark, centred in the card at 48px (#8790 D3). */
|
|
33
|
+
logo?: ReactNode;
|
|
32
34
|
/** The `?token=` Better Auth redirected back with. May be empty. */
|
|
33
35
|
token: string;
|
|
34
36
|
/** Where a completed reset lands. */
|
|
@@ -60,6 +62,7 @@ export function ResetPasswordCard({
|
|
|
60
62
|
labels,
|
|
61
63
|
errorLabels,
|
|
62
64
|
navigate = defaultNavigate,
|
|
65
|
+
logo,
|
|
63
66
|
}: ResetPasswordCardProps): ReactElement {
|
|
64
67
|
const [errors, setErrors] = useState<{
|
|
65
68
|
password?: string;
|
|
@@ -104,6 +107,7 @@ export function ResetPasswordCard({
|
|
|
104
107
|
return (
|
|
105
108
|
<div className="flex flex-col gap-4" data-testid="reset-password-page">
|
|
106
109
|
<AuthCard
|
|
110
|
+
{...(logo !== undefined ? { logo } : {})}
|
|
107
111
|
intent="reset"
|
|
108
112
|
signInHref={signInHref}
|
|
109
113
|
labels={labels}
|