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
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for SignUpCard's sign-up agreements (#8438 D13, AC-3
|
|
2
|
+
* Tests for SignUpCard's sign-up agreements (#8438 D13, AC-3; #8765 D10,
|
|
3
|
+
* AC-1).
|
|
3
4
|
*
|
|
4
5
|
* Every submit — the password form and each OAuth provider — is gated by
|
|
5
6
|
* the site's `signUp` agreements, rendered as a `ConsentGroup` below
|
|
6
7
|
* `AuthCard`. Nothing happens on any entry point until every required
|
|
7
|
-
* agreement is ticked. The
|
|
8
|
-
*
|
|
8
|
+
* agreement is ticked. The ticked ids then travel to the server, which
|
|
9
|
+
* decides: in the sign-up body, and in a provider launch's `additionalData`.
|
|
10
|
+
* A server refusal (`AGREEMENTS_REQUIRED`) is shown on the agreements. No
|
|
11
|
+
* cookie carries them any more.
|
|
9
12
|
*/
|
|
10
|
-
import { render, screen } from "@testing-library/react";
|
|
13
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
11
14
|
import userEvent from "@testing-library/user-event";
|
|
12
15
|
|
|
16
|
+
import messages from "../../../../../messages/en.json";
|
|
17
|
+
|
|
13
18
|
const mockSignUpEmail = jest.fn();
|
|
14
19
|
const mockSignInSocial = jest.fn();
|
|
15
20
|
jest.mock("better-auth/react", () => ({
|
|
@@ -20,13 +25,6 @@ jest.mock("better-auth/react", () => ({
|
|
|
20
25
|
}),
|
|
21
26
|
}));
|
|
22
27
|
|
|
23
|
-
const mockRememberAgreements = jest.fn();
|
|
24
|
-
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: the Server Action module is a "use server" boundary; the card only passes it through.
|
|
25
|
-
jest.mock("../sign-up.actions", () => ({
|
|
26
|
-
__esModule: true,
|
|
27
|
-
rememberSignUpAgreementsAction: (...args: unknown[]) => mockRememberAgreements(...args),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
28
|
import { SignUpCard, type SignUpCardProps } from "../sign-up-card";
|
|
31
29
|
|
|
32
30
|
const AGREEMENT = {
|
|
@@ -55,10 +53,15 @@ function props(overrides: Partial<SignUpCardProps> = {}): SignUpCardProps {
|
|
|
55
53
|
orContinue: "or continue with",
|
|
56
54
|
},
|
|
57
55
|
notConfiguredSubhead: "Sign-up is not configured.",
|
|
58
|
-
errorLabels: {
|
|
56
|
+
errorLabels: {
|
|
57
|
+
signUpFailed: "Sign-up failed.",
|
|
58
|
+
providerFailed: "Provider failed.",
|
|
59
|
+
agreementsRequired: messages.auth.signUpAgreementsRequired,
|
|
60
|
+
},
|
|
59
61
|
agreements: [AGREEMENT],
|
|
60
62
|
agreementPolicies: { terms: "/terms", privacy: "/privacy" },
|
|
61
63
|
agreementsLegend: "Agreements",
|
|
64
|
+
locale: "en",
|
|
62
65
|
...overrides,
|
|
63
66
|
};
|
|
64
67
|
}
|
|
@@ -78,7 +81,6 @@ async function tick(purposeId: string): Promise<void> {
|
|
|
78
81
|
beforeEach(() => {
|
|
79
82
|
mockSignUpEmail.mockReset().mockResolvedValue({ data: { status: true }, error: null });
|
|
80
83
|
mockSignInSocial.mockReset().mockResolvedValue({ data: { status: true }, error: null });
|
|
81
|
-
mockRememberAgreements.mockReset().mockResolvedValue(undefined);
|
|
82
84
|
});
|
|
83
85
|
|
|
84
86
|
describe("the password submit is gated (AC-3)", () => {
|
|
@@ -92,23 +94,31 @@ describe("the password submit is gated (AC-3)", () => {
|
|
|
92
94
|
render(<SignUpCard {...props()} />);
|
|
93
95
|
await fillAndSubmit();
|
|
94
96
|
expect(mockSignUpEmail).not.toHaveBeenCalled();
|
|
95
|
-
expect(mockRememberAgreements).not.toHaveBeenCalled();
|
|
96
97
|
});
|
|
97
98
|
|
|
98
|
-
it("
|
|
99
|
+
it("signs up once every required agreement is ticked, the body carrying agreements and locale (#8765 AC-1)", async () => {
|
|
99
100
|
render(<SignUpCard {...props()} />);
|
|
100
101
|
await tick("account_terms");
|
|
101
102
|
expect(screen.getByTestId("sign-up-card-gate")).toHaveAttribute("aria-disabled", "false");
|
|
102
103
|
await fillAndSubmit();
|
|
103
|
-
expect(mockRememberAgreements).toHaveBeenCalledWith("ada@example.com", ["account_terms"]);
|
|
104
104
|
expect(mockSignUpEmail).toHaveBeenCalledWith({
|
|
105
105
|
email: "ada@example.com",
|
|
106
106
|
password: "correct horse battery staple",
|
|
107
107
|
name: "ada@example.com",
|
|
108
108
|
callbackURL: "/account",
|
|
109
|
+
agreements: ["account_terms"],
|
|
110
|
+
locale: "en",
|
|
109
111
|
});
|
|
110
112
|
});
|
|
111
113
|
|
|
114
|
+
it("sets no cookie — the server writes the grant with the user (#8765 AC-1)", async () => {
|
|
115
|
+
render(<SignUpCard {...props()} />);
|
|
116
|
+
await tick("account_terms");
|
|
117
|
+
await fillAndSubmit();
|
|
118
|
+
expect(mockSignUpEmail).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(document.cookie).toBe("");
|
|
120
|
+
});
|
|
121
|
+
|
|
112
122
|
it("re-disables after unticking", async () => {
|
|
113
123
|
render(<SignUpCard {...props()} />);
|
|
114
124
|
await tick("account_terms");
|
|
@@ -137,12 +147,36 @@ describe("the password submit is gated (AC-3)", () => {
|
|
|
137
147
|
expect(mockSignUpEmail).toHaveBeenCalledTimes(1);
|
|
138
148
|
});
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("the server's answer (#8765 AC-1)", () => {
|
|
153
|
+
it("an AGREEMENTS_REQUIRED refusal renders auth.signUpAgreementsRequired inside sign-up-agreements, not on the password field", async () => {
|
|
154
|
+
mockSignUpEmail.mockResolvedValue({
|
|
155
|
+
data: null,
|
|
156
|
+
error: { code: "AGREEMENTS_REQUIRED", message: "Accept the agreements to create an account.", status: 400 },
|
|
157
|
+
});
|
|
158
|
+
render(<SignUpCard {...props()} />);
|
|
159
|
+
await tick("account_terms");
|
|
160
|
+
await fillAndSubmit();
|
|
161
|
+
const agreements = screen.getByTestId("sign-up-agreements");
|
|
162
|
+
await waitFor(() =>
|
|
163
|
+
expect(within(agreements).getByRole("alert")).toHaveTextContent(
|
|
164
|
+
messages.auth.signUpAgreementsRequired,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
expect(screen.queryByText("Sign-up failed.")).not.toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("any other failure keeps the password-field message and no agreements error", async () => {
|
|
171
|
+
mockSignUpEmail.mockResolvedValue({
|
|
172
|
+
data: null,
|
|
173
|
+
error: { code: "PASSWORD_TOO_SHORT", message: "Password too short", status: 400 },
|
|
174
|
+
});
|
|
142
175
|
render(<SignUpCard {...props()} />);
|
|
143
176
|
await tick("account_terms");
|
|
144
177
|
await fillAndSubmit();
|
|
145
|
-
expect(
|
|
178
|
+
expect(await screen.findByText("Sign-up failed.")).toBeInTheDocument();
|
|
179
|
+
expect(within(screen.getByTestId("sign-up-agreements")).queryByRole("alert")).toBeNull();
|
|
146
180
|
});
|
|
147
181
|
});
|
|
148
182
|
|
|
@@ -154,12 +188,16 @@ describe("provider buttons are gated too (AC-3)", () => {
|
|
|
154
188
|
expect(mockSignInSocial).not.toHaveBeenCalled();
|
|
155
189
|
});
|
|
156
190
|
|
|
157
|
-
it("launches the provider once ticked,
|
|
191
|
+
it("launches the provider once ticked, the agreements in its additionalData (#8765 AC-1)", async () => {
|
|
158
192
|
render(<SignUpCard {...props({ method: "sso", providers: ["google"] })} />);
|
|
159
193
|
await tick("account_terms");
|
|
160
194
|
const user = userEvent.setup();
|
|
161
195
|
await user.click(screen.getByTestId("auth-sso-google"));
|
|
162
|
-
expect(mockSignInSocial).toHaveBeenCalledWith({
|
|
163
|
-
|
|
196
|
+
expect(mockSignInSocial).toHaveBeenCalledWith({
|
|
197
|
+
provider: "google",
|
|
198
|
+
callbackURL: "/account",
|
|
199
|
+
additionalData: { agreements: ["account_terms"], locale: "en" },
|
|
200
|
+
});
|
|
201
|
+
expect(document.cookie).toBe("");
|
|
164
202
|
});
|
|
165
203
|
});
|
|
@@ -25,8 +25,10 @@ import { getTranslations } from "next-intl/server";
|
|
|
25
25
|
import type { ReactElement } from "react";
|
|
26
26
|
|
|
27
27
|
import { getActiveProviderIds, isSignUpOpen } from "../../../../lib/auth-env";
|
|
28
|
+
import { consentPoliciesFor } from "../../../../lib/consent-policies";
|
|
28
29
|
|
|
29
30
|
import { SignUpCard, type SignUpProvider } from "./sign-up-card";
|
|
31
|
+
import { siteLogo } from "../../../../lib/site-logo";
|
|
30
32
|
|
|
31
33
|
interface Props {
|
|
32
34
|
params: Promise<{ locale: string }>;
|
|
@@ -55,6 +57,7 @@ export default async function SignUpPage({
|
|
|
55
57
|
return (
|
|
56
58
|
<div className="flex flex-col gap-4" data-testid="sign-up-page">
|
|
57
59
|
<AuthCard
|
|
60
|
+
logo={siteLogo()}
|
|
58
61
|
intent="invitation"
|
|
59
62
|
signInHref={signInHref}
|
|
60
63
|
labels={closedLabels}
|
|
@@ -87,16 +90,19 @@ export default async function SignUpPage({
|
|
|
87
90
|
// #8438 D13 — the site's sign-up agreements, resolved to this locale. No
|
|
88
91
|
// per-site registry row is wired yet (C1), so this always reads the
|
|
89
92
|
// platform default — the same registry the member Privacy tab and the
|
|
90
|
-
// account-settings write both read from.
|
|
91
|
-
|
|
92
|
-
const agreements = signUpAgreements(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// account-settings write both read from. Their links resolve through the
|
|
94
|
+
// one localized policy map every agreement surface uses (#8765 D9).
|
|
95
|
+
const agreements = signUpAgreements(SITE_CONSENT_CONFIG_DEFAULTS).map(
|
|
96
|
+
(purpose) => ({
|
|
97
|
+
id: purpose.id,
|
|
98
|
+
required: purpose.required,
|
|
99
|
+
statement: resolveConsentStatement(purpose, locale).statement,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
97
102
|
|
|
98
103
|
return (
|
|
99
104
|
<SignUpCard
|
|
105
|
+
logo={siteLogo()}
|
|
100
106
|
providers={providers}
|
|
101
107
|
method={method}
|
|
102
108
|
next={localeHref(locale, "/account")}
|
|
@@ -106,10 +112,12 @@ export default async function SignUpPage({
|
|
|
106
112
|
errorLabels={{
|
|
107
113
|
signUpFailed: t("signUpFailed"),
|
|
108
114
|
providerFailed: t("providerFailed"),
|
|
115
|
+
agreementsRequired: t("signUpAgreementsRequired"),
|
|
109
116
|
}}
|
|
110
117
|
agreements={agreements}
|
|
111
|
-
agreementPolicies={
|
|
118
|
+
agreementPolicies={consentPoliciesFor(locale)}
|
|
112
119
|
agreementsLegend={t("signUpAgreementsLegend")}
|
|
120
|
+
locale={locale}
|
|
113
121
|
/>
|
|
114
122
|
);
|
|
115
123
|
}
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
* nothing until every required agreement is ticked. A wrapping div's
|
|
29
29
|
* `aria-disabled` + `pointer-events-none` styling carries the same state
|
|
30
30
|
* visually for a mouse/touch pointer in a real browser.
|
|
31
|
+
*
|
|
32
|
+
* #8765 D10 — the gate above is a convenience; the server decides. The
|
|
33
|
+
* password submit sends the ticked ids as `agreements`, with `locale`, in the
|
|
34
|
+
* sign-up body, and Better Auth refuses a body without them
|
|
35
|
+
* (`AGREEMENTS_REQUIRED`, shown on the agreements, not the password field).
|
|
36
|
+
* Each provider click carries the same pair as `signIn.social`'s
|
|
37
|
+
* `additionalData`, which Better Auth keeps in the OAuth state for the
|
|
38
|
+
* callback. The server writes the grants with the user
|
|
39
|
+
* (`lib/sign-up-agreements.ts`); no cookie carries them.
|
|
31
40
|
*/
|
|
32
41
|
|
|
33
42
|
import {
|
|
@@ -37,12 +46,11 @@ import {
|
|
|
37
46
|
type AuthSubmitData,
|
|
38
47
|
} from "@working-theory/ui/blocks";
|
|
39
48
|
import { Alert, AlertDescription } from "@working-theory/ui/primitives";
|
|
40
|
-
import {
|
|
49
|
+
import { SIGN_UP_AGREEMENTS_FIELD } from "@working-theory/validation";
|
|
50
|
+
import { useState, type MouseEvent, type ReactElement, type ReactNode } from "react";
|
|
41
51
|
|
|
42
52
|
import { authClient, useAuthCardFlow } from "../use-auth-card-flow";
|
|
43
53
|
|
|
44
|
-
import { rememberSignUpAgreementsAction } from "./sign-up.actions";
|
|
45
|
-
|
|
46
54
|
/** The OAuth providers this page can launch — a subset of AuthCard's. */
|
|
47
55
|
export type SignUpProvider = "google" | "github";
|
|
48
56
|
|
|
@@ -55,6 +63,8 @@ export interface SignUpAgreement {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
export interface SignUpCardProps {
|
|
66
|
+
/** The site's mark, centred in the card at 48px (#8790 D3). */
|
|
67
|
+
logo?: ReactNode;
|
|
58
68
|
/** Active OAuth providers, exactly as auth-env reports them. */
|
|
59
69
|
providers: SignUpProvider[];
|
|
60
70
|
/** "password" when the email provider is active; otherwise "sso". */
|
|
@@ -68,16 +78,35 @@ export interface SignUpCardProps {
|
|
|
68
78
|
/** Subhead shown when no provider is configured. */
|
|
69
79
|
notConfiguredSubhead: string;
|
|
70
80
|
/** Translated error copy. */
|
|
71
|
-
errorLabels: {
|
|
81
|
+
errorLabels: {
|
|
82
|
+
signUpFailed: string;
|
|
83
|
+
providerFailed: string;
|
|
84
|
+
/** Shown on the agreements when the server answers `AGREEMENTS_REQUIRED`. */
|
|
85
|
+
agreementsRequired: string;
|
|
86
|
+
};
|
|
72
87
|
/** The site's `signUp` agreements — always at least `account_terms`. */
|
|
73
88
|
agreements: SignUpAgreement[];
|
|
74
89
|
/** Policy key → href, for the statements' inline links (D1/D3). */
|
|
75
90
|
agreementPolicies: Record<string, string>;
|
|
76
91
|
/** The consent fieldset's legend (`auth.signUp.agreementsLegend`). */
|
|
77
92
|
agreementsLegend: string;
|
|
93
|
+
/** The page's locale, sent with the agreements (the statements' language). */
|
|
94
|
+
locale: string;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
/** The server's body key for the ticked ids — also the group's control name. */
|
|
98
|
+
const AGREEMENTS_FIELD_NAME = SIGN_UP_AGREEMENTS_FIELD;
|
|
99
|
+
|
|
100
|
+
/** The server's refusal of a sign-up without the agreements (#8765 D3). */
|
|
101
|
+
const AGREEMENTS_REQUIRED = "AGREEMENTS_REQUIRED";
|
|
102
|
+
|
|
103
|
+
function errorCode(result: unknown): unknown {
|
|
104
|
+
if (typeof result !== "object" || result === null) return undefined;
|
|
105
|
+
const error: unknown = Reflect.get(result, "error");
|
|
106
|
+
return typeof error === "object" && error !== null
|
|
107
|
+
? Reflect.get(error, "code")
|
|
108
|
+
: undefined;
|
|
109
|
+
}
|
|
81
110
|
|
|
82
111
|
export function SignUpCard({
|
|
83
112
|
providers,
|
|
@@ -90,6 +119,8 @@ export function SignUpCard({
|
|
|
90
119
|
agreements,
|
|
91
120
|
agreementPolicies,
|
|
92
121
|
agreementsLegend,
|
|
122
|
+
logo,
|
|
123
|
+
locale,
|
|
93
124
|
}: SignUpCardProps): ReactElement {
|
|
94
125
|
const { errors, providerError, launchProvider, submitPassword } =
|
|
95
126
|
useAuthCardFlow(next, {
|
|
@@ -97,9 +128,21 @@ export function SignUpCard({
|
|
|
97
128
|
passwordFailed: errorLabels.signUpFailed,
|
|
98
129
|
});
|
|
99
130
|
|
|
100
|
-
const requiredAgreementIds = agreements
|
|
131
|
+
const requiredAgreementIds = agreements
|
|
132
|
+
.filter((a) => a.required)
|
|
133
|
+
.map((a) => a.id);
|
|
101
134
|
const [ticked, setTicked] = useState<ReadonlySet<string>>(new Set());
|
|
135
|
+
const [agreementsError, setAgreementsError] = useState<string | undefined>(
|
|
136
|
+
undefined,
|
|
137
|
+
);
|
|
102
138
|
const allAgreed = requiredAgreementIds.every((id) => ticked.has(id));
|
|
139
|
+
// What the server checks and records (D10): the ticked ids, in the order shown.
|
|
140
|
+
const agreed = (): { agreements: string[]; locale: string } => ({
|
|
141
|
+
[SIGN_UP_AGREEMENTS_FIELD]: agreements
|
|
142
|
+
.filter((a) => ticked.has(a.id))
|
|
143
|
+
.map((a) => a.id),
|
|
144
|
+
locale,
|
|
145
|
+
});
|
|
103
146
|
|
|
104
147
|
// `ConsentGroup`'s checkboxes are uncontrolled, by design (#5275), and
|
|
105
148
|
// always start unticked (no `defaults` are ever passed here) — so a click
|
|
@@ -107,7 +150,9 @@ export function SignUpCard({
|
|
|
107
150
|
// reads the Radix root's own post-click DOM state, which a same-tick
|
|
108
151
|
// ancestor listener could still see as stale.
|
|
109
152
|
function handleAgreementsClick(event: MouseEvent<HTMLDivElement>): void {
|
|
110
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>(
|
|
153
|
+
const target = (event.target as HTMLElement).closest<HTMLElement>(
|
|
154
|
+
'[role="checkbox"]',
|
|
155
|
+
);
|
|
111
156
|
if (!target?.id) return;
|
|
112
157
|
const purposeId = target.id.slice(`${AGREEMENTS_FIELD_NAME}.`.length);
|
|
113
158
|
setTicked((previous) => {
|
|
@@ -124,35 +169,36 @@ export function SignUpCard({
|
|
|
124
169
|
provider,
|
|
125
170
|
// Gated: a provider launch that would create the account is refused
|
|
126
171
|
// until every required agreement is ticked, exactly as the password
|
|
127
|
-
// submit below.
|
|
128
|
-
// callback carries no form data this component could read an email
|
|
129
|
-
// from, so the account home's explicit "Otherwise" card asks instead.
|
|
172
|
+
// submit below. The agreements ride in the OAuth state (D10).
|
|
130
173
|
onClick: () => {
|
|
131
174
|
if (!allAgreed) return;
|
|
132
|
-
void launchProvider(provider);
|
|
175
|
+
void launchProvider(provider, agreed());
|
|
133
176
|
},
|
|
134
177
|
}));
|
|
135
178
|
|
|
136
179
|
const handleSubmit = (data: AuthSubmitData): void => {
|
|
137
180
|
if (!allAgreed) return;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
181
|
+
setAgreementsError(undefined);
|
|
182
|
+
// Better Auth requires a `name` on email sign-up; this card collects
|
|
183
|
+
// none, so the address doubles as the display name until the person
|
|
184
|
+
// sets one. Better Auth ignores body keys it does not know, so
|
|
185
|
+
// `agreements` and `locale` never reach the User row.
|
|
186
|
+
const outcome = authClient.signUp.email({
|
|
187
|
+
email: data.email,
|
|
188
|
+
password: data.password ?? "",
|
|
189
|
+
name: data.email,
|
|
190
|
+
callbackURL: next,
|
|
191
|
+
...agreed(),
|
|
192
|
+
});
|
|
193
|
+
// The agreements refusal belongs to the agreements, not the password
|
|
194
|
+
// field; any other failure keeps the password-field message.
|
|
195
|
+
void submitPassword(
|
|
196
|
+
outcome.then((result) => {
|
|
197
|
+
if (errorCode(result) !== AGREEMENTS_REQUIRED) return result;
|
|
198
|
+
setAgreementsError(errorLabels.agreementsRequired);
|
|
199
|
+
return null;
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
156
202
|
};
|
|
157
203
|
|
|
158
204
|
return (
|
|
@@ -168,6 +214,7 @@ export function SignUpCard({
|
|
|
168
214
|
data-testid="sign-up-card-gate"
|
|
169
215
|
>
|
|
170
216
|
<AuthCard
|
|
217
|
+
{...(logo !== undefined ? { logo } : {})}
|
|
171
218
|
intent="sign-up"
|
|
172
219
|
method={method}
|
|
173
220
|
signInHref={signInHref}
|
|
@@ -178,13 +225,20 @@ export function SignUpCard({
|
|
|
178
225
|
labels={labels}
|
|
179
226
|
/>
|
|
180
227
|
</div>
|
|
181
|
-
{/* eslint-disable-next-line -- delegated-click capture over ConsentGroup's own real `role="checkbox"` descendants (Radix), which are themselves already keyboard-operable and dispatch a native click on Enter/Space; this wrapper adds no interactive semantics of its own and is never itself focused. (Bare directive, not named rules: `jsx-a11y/*` is registered in this monorepo's own lint but not in a scaffolded site's trimmed eslint config
|
|
182
|
-
<div
|
|
228
|
+
{/* eslint-disable-next-line -- delegated-click capture over ConsentGroup's own real `role="checkbox"` descendants (Radix), which are themselves already keyboard-operable and dispatch a native click on Enter/Space; this wrapper adds no interactive semantics of its own and is never itself focused. (Bare directive, not named rules: `jsx-a11y/*` is registered in this monorepo's own lint but not in a scaffolded site's trimmed eslint config.) */}
|
|
229
|
+
<div
|
|
230
|
+
onClick={handleAgreementsClick}
|
|
231
|
+
// Below `md` the card is borderless with a 20px inset; the agreements
|
|
232
|
+
// keep that inset so they line up with its fields.
|
|
233
|
+
className="px-5 md:px-0"
|
|
234
|
+
data-testid="sign-up-agreements"
|
|
235
|
+
>
|
|
183
236
|
<ConsentGroup
|
|
184
237
|
name={AGREEMENTS_FIELD_NAME}
|
|
185
238
|
legend={agreementsLegend}
|
|
186
239
|
purposes={agreements}
|
|
187
240
|
policies={agreementPolicies}
|
|
241
|
+
error={agreementsError}
|
|
188
242
|
/>
|
|
189
243
|
</div>
|
|
190
244
|
</div>
|
|
@@ -47,8 +47,15 @@ export interface AuthCardFlow {
|
|
|
47
47
|
errors: FieldErrors;
|
|
48
48
|
/** The SSO-body error banner text, or null when none is pending. */
|
|
49
49
|
providerError: string | null;
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Launches an OAuth provider via `signIn.social`. `additionalData` rides
|
|
52
|
+
* in Better Auth's OAuth state to the callback — the sign-up card's ticked
|
|
53
|
+
* agreements and locale (#8765 D10); the sign-in card passes none.
|
|
54
|
+
*/
|
|
55
|
+
launchProvider: (
|
|
56
|
+
provider: AuthProvider,
|
|
57
|
+
additionalData?: Record<string, unknown>,
|
|
58
|
+
) => Promise<void>;
|
|
52
59
|
/**
|
|
53
60
|
* Awaits the caller's own Better Auth call (`signIn.email(...)` or
|
|
54
61
|
* `signUp.email(...)`) and surfaces a `{ error }` result — or a rejection —
|
|
@@ -68,12 +75,19 @@ export function useAuthCardFlow(
|
|
|
68
75
|
const [errors, setErrors] = useState<FieldErrors>({});
|
|
69
76
|
const [providerError, setProviderError] = useState<string | null>(null);
|
|
70
77
|
|
|
71
|
-
const launchProvider = async (
|
|
78
|
+
const launchProvider = async (
|
|
79
|
+
provider: AuthProvider,
|
|
80
|
+
additionalData?: Record<string, unknown>,
|
|
81
|
+
): Promise<void> => {
|
|
72
82
|
setProviderError(null);
|
|
73
83
|
// Better Auth reports a failed launch as `{ error }`; a network/runtime
|
|
74
84
|
// failure rejects. Both land on the same providerFailed message.
|
|
75
85
|
const failed = await authClient.signIn
|
|
76
|
-
.social({
|
|
86
|
+
.social({
|
|
87
|
+
provider,
|
|
88
|
+
callbackURL: next,
|
|
89
|
+
...(additionalData ? { additionalData } : {}),
|
|
90
|
+
})
|
|
77
91
|
.then(
|
|
78
92
|
(res: AuthResult) => Boolean(res?.error),
|
|
79
93
|
() => true,
|
|
@@ -21,6 +21,7 @@ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
|
21
21
|
import type { Metadata } from "next";
|
|
22
22
|
import { getTranslations } from "next-intl/server";
|
|
23
23
|
import type { ReactElement } from "react";
|
|
24
|
+
import { siteLogo } from "../../../../lib/site-logo";
|
|
24
25
|
|
|
25
26
|
interface Props {
|
|
26
27
|
params: Promise<{ locale: string }>;
|
|
@@ -63,6 +64,7 @@ export default async function VerifyEmailPage({
|
|
|
63
64
|
<div className="flex flex-col gap-4" data-testid="verify-email-page">
|
|
64
65
|
<div data-testid="verify-token-error">
|
|
65
66
|
<AuthCard
|
|
67
|
+
logo={siteLogo()}
|
|
66
68
|
intent="invitation"
|
|
67
69
|
signInHref={signInHref}
|
|
68
70
|
labels={labels}
|
|
@@ -81,7 +83,7 @@ export default async function VerifyEmailPage({
|
|
|
81
83
|
|
|
82
84
|
return (
|
|
83
85
|
<div className="flex flex-col gap-4" data-testid="verify-email-page">
|
|
84
|
-
<AuthCard intent="sent" signInHref={signInHref} labels={labels} />
|
|
86
|
+
<AuthCard logo={siteLogo()} intent="sent" signInHref={signInHref} labels={labels} />
|
|
85
87
|
</div>
|
|
86
88
|
);
|
|
87
89
|
}
|
|
@@ -348,8 +348,10 @@ describe("AuthedLayout — the email sign-in link (#7435)", () => {
|
|
|
348
348
|
configureEmailLink();
|
|
349
349
|
await renderSignedOutLayout();
|
|
350
350
|
expect(screen.getByTestId("sign-in-gate")).toBeInTheDocument();
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
// #8790 D4/D5: one card — Google is a social button under the "or", and the
|
|
352
|
+
// sign-in link is the card's primary action.
|
|
353
|
+
expect(screen.getByTestId("auth-social-google")).toBeInTheDocument();
|
|
354
|
+
expect(screen.getByTestId("auth-submit")).toHaveTextContent(
|
|
353
355
|
"copy:emailLinkSubmit",
|
|
354
356
|
);
|
|
355
357
|
expect(screen.queryByTestId("auth-panel-password")).not.toBeInTheDocument();
|
|
@@ -363,7 +365,8 @@ describe("AuthedLayout — the email sign-in link (#7435)", () => {
|
|
|
363
365
|
it("renders the email-link form, not the EmptyState, when it is the only method", async () => {
|
|
364
366
|
configureEmailLink();
|
|
365
367
|
await renderSignedOutLayout();
|
|
366
|
-
expect(screen.getByTestId("auth-email
|
|
368
|
+
expect(screen.getByTestId("auth-email")).toBeInTheDocument();
|
|
369
|
+
expect(screen.getByTestId("auth-submit")).toBeInTheDocument();
|
|
367
370
|
expect(screen.queryByTestId("sign-in-gate-empty")).not.toBeInTheDocument();
|
|
368
371
|
expect(screen.getByTestId("sign-in-gate-link")).toBeInTheDocument();
|
|
369
372
|
});
|
|
@@ -378,7 +381,7 @@ describe("AuthedLayout — the email sign-in link (#7435)", () => {
|
|
|
378
381
|
configureEmailLink();
|
|
379
382
|
delete process.env[name];
|
|
380
383
|
await renderSignedOutLayout();
|
|
381
|
-
expect(screen.queryByTestId("auth-email
|
|
384
|
+
expect(screen.queryByTestId("auth-email")).not.toBeInTheDocument();
|
|
382
385
|
expect(screen.getByTestId("auth-panel-provider-google")).toBeInTheDocument();
|
|
383
386
|
});
|
|
384
387
|
});
|