create-ailk 0.20.0 → 0.22.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.
Files changed (222) hide show
  1. package/README.md +21 -1
  2. package/component-catalog.json +220 -8
  3. package/component-catalog.md +21 -9
  4. package/dist/add-module.js +9 -5
  5. package/dist/apply-design.d.ts +9 -0
  6. package/dist/apply-design.js +78 -31
  7. package/dist/derive-architecture.d.ts +8 -1
  8. package/dist/derive-architecture.js +38 -3
  9. package/dist/design-home.d.ts +54 -11
  10. package/dist/design-home.js +160 -22
  11. package/dist/design-package.js +7 -2
  12. package/dist/lib/design-checks.d.ts +6 -1
  13. package/dist/lib/design-checks.js +23 -2
  14. package/dist/lib/env-catalog.js +5 -1
  15. package/dist/module-architecture.js +4 -2
  16. package/dist/programmatic.js +63 -25
  17. package/dist/site-chrome.d.ts +8 -0
  18. package/dist/site-chrome.js +17 -1
  19. package/dist/site-roster.d.ts +4 -2
  20. package/dist/site-roster.js +7 -5
  21. package/dist/spoke-checkout.d.ts +39 -18
  22. package/dist/spoke-checkout.js +177 -236
  23. package/dist/starter-grid.js +16 -0
  24. package/dist/surfaces.js +30 -14
  25. package/dist/sync-routes.d.ts +28 -0
  26. package/dist/sync-routes.js +124 -1
  27. package/dist/template-record.d.ts +4 -3
  28. package/dist/template-record.js +6 -3
  29. package/dist/topology.js +25 -1
  30. package/dist/update.d.ts +6 -1
  31. package/dist/update.js +125 -18
  32. package/package.json +3 -3
  33. package/site-templates/_base/ailk-template.json +2 -2
  34. package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
  35. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  36. package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
  37. package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
  38. package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
  39. package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
  41. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
  43. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
  46. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
  48. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
  49. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
  50. package/templates/README.md +26 -25
  51. package/templates/apps/api/CLAUDE.md +20 -7
  52. package/templates/apps/api/jest.config.cjs +1 -0
  53. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  54. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  55. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  56. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  57. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  58. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  59. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  60. package/templates/apps/api/src/middleware/auth.ts +13 -6
  61. package/templates/apps/api/src/openapi/spec.ts +30 -3
  62. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  63. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  64. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  65. package/templates/apps/api/src/routes/consent/account.ts +266 -0
  66. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  67. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  68. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  69. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  70. package/templates/apps/api/src/routes/leads/index.ts +161 -7
  71. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
  72. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  73. package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
  74. package/templates/apps/api/src/server.ts +28 -0
  75. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
  76. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  77. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  78. package/templates/apps/api/src/services/consent.ts +95 -0
  79. package/templates/apps/api/src/services/lead-routing.ts +337 -8
  80. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  81. package/templates/apps/api/src/services/person.ts +40 -8
  82. package/templates/apps/api/src/services/plan-offerings.ts +59 -0
  83. package/templates/apps/api/src/services/project-listings.ts +6 -1
  84. package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
  85. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
  86. package/templates/apps/mcp/jest.config.cjs +1 -0
  87. package/templates/apps/web/.env.example +14 -0
  88. package/templates/apps/web/CLAUDE.md +139 -3
  89. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  90. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  91. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  92. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  93. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  94. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  95. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  96. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  97. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  98. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  99. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  100. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  101. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  102. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  103. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  104. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  105. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  106. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  114. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  115. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  116. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  117. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  118. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  119. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  120. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  121. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  122. package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
  123. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
  124. package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
  125. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
  126. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
  127. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
  128. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
  129. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
  130. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
  131. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
  132. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  133. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  134. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  135. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  136. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  137. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
  139. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  140. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
  141. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
  143. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
  144. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
  145. package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
  146. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
  147. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
  148. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  149. package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
  150. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  151. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
  152. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  153. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
  154. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  155. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  156. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  157. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  158. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  159. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  160. package/templates/apps/web/app/[locale]/layout.tsx +23 -2
  161. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  162. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  163. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  164. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  165. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  166. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  167. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  168. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  169. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  170. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  171. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  172. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  173. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  174. package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
  175. package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
  176. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  177. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  178. package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
  179. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
  180. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
  181. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  182. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  183. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
  184. package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
  185. package/templates/apps/web/auth.ts +28 -2
  186. package/templates/apps/web/jest.config.cjs +1 -0
  187. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  188. package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
  189. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  190. package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
  191. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
  192. package/templates/apps/web/lib/account-invite.ts +37 -0
  193. package/templates/apps/web/lib/checkout-config.ts +27 -0
  194. package/templates/apps/web/lib/checkout.ts +93 -0
  195. package/templates/apps/web/lib/consent-policies.ts +22 -0
  196. package/templates/apps/web/lib/deck-page.ts +192 -0
  197. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  198. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  199. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  200. package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
  201. package/templates/apps/web/lib/site-chrome.ts +2 -0
  202. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  203. package/templates/apps/web/messages/en.json +40 -12
  204. package/templates/apps/web/package.json +1 -0
  205. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  206. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  207. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  208. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  209. package/templates/database/CHANGELOG.md +47 -0
  210. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  211. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  212. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  213. package/templates/database/__tests__/seed.test.ts +72 -2
  214. package/templates/database/inbox/schema.prisma +17 -2
  215. package/templates/database/jest.config.cjs +1 -0
  216. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  217. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  218. package/templates/database/package.json +1 -1
  219. package/templates/database/seed.ts +60 -0
  220. package/templates/tools/jest/pin-timezone.cjs +34 -0
  221. package/versions.json +34 -34
  222. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
@@ -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
- * The mark is clamped to 28px the way `BrandLogo` clamps it inside
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}
@@ -21,6 +21,9 @@
21
21
  * (h) the email sign-in link (#7435): with "magic-link" active the form
22
22
  * renders beside the providers (or alone); a `?error=` from a failed
23
23
  * link maps to fixed copy and is never echoed.
24
+ * (i) the hinted email (#8757 D9): a valid single `?email=` is the starting
25
+ * value of every email input of the card; an invalid or repeated value,
26
+ * or none, leaves them empty as before. It signs no one in.
24
27
  *
25
28
  * `next-intl/server` is mocked globally (apps/web/__tests__/setup.ts) against
26
29
  * the real en.json, so `signIn` copy is asserted by its real strings.
@@ -58,6 +61,21 @@ jest.mock("better-auth/react", () => ({
58
61
  }),
59
62
  }));
60
63
 
64
+ // #8790 D2 — the spoke back link. The hub's secret and the registry read are
65
+ // the two boundaries the page crosses; the continuation itself is REAL
66
+ // (minted below with the same test secret), so the sealed-ref path is covered.
67
+ const TEST_SECRET = "test-secret-for-the-sign-in-page-suite";
68
+ // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts is Better Auth's server singleton (a DB/HTTP boundary)
69
+ jest.mock("../../../../../auth", () => ({
70
+ __esModule: true,
71
+ resolveSecret: () => TEST_SECRET,
72
+ }));
73
+ const mockClientInfo = jest.fn();
74
+ jest.mock("@working-theory/api-client", () => ({
75
+ __esModule: true,
76
+ hubSignIn: { clientInfo: (...args: unknown[]) => mockClientInfo(...args) },
77
+ }));
78
+ import { mintContinuation } from "../../../../api/auth/hub/continuation";
61
79
  import en from "../../../../../messages/en.json";
62
80
  import Page, { generateMetadata } from "../page";
63
81
 
@@ -124,7 +142,9 @@ describe("/sign-in page", () => {
124
142
  expect(screen.queryByTestId("auth-sso-github")).toBeNull();
125
143
  expect(screen.queryByTestId("auth-submit")).toBeNull();
126
144
  expect(screen.queryByTestId("auth-email")).toBeNull();
127
- expect(screen.getByText(en.signIn.notConfiguredSubhead)).toBeInTheDocument();
145
+ expect(
146
+ screen.getByText(en.signIn.notConfiguredSubhead),
147
+ ).toBeInTheDocument();
128
148
  expect(mockSocial).not.toHaveBeenCalled();
129
149
  });
130
150
 
@@ -144,7 +164,9 @@ describe("/sign-in page", () => {
144
164
  mockSocial.mockResolvedValue({ data: null, error: { message: "nope" } });
145
165
  await renderPage("/account");
146
166
  await user.click(screen.getByTestId("auth-sso-google"));
147
- expect(await screen.findByText(en.signIn.providerFailed)).toBeInTheDocument();
167
+ expect(
168
+ await screen.findByText(en.signIn.providerFailed),
169
+ ).toBeInTheDocument();
148
170
  });
149
171
 
150
172
  it("(e) a protocol-relative ?next= falls back to /account", async () => {
@@ -213,27 +235,49 @@ describe("/sign-in page — the email sign-in link (#7435)", () => {
213
235
  it("(h) with google + magic-link: the Google button and 'Email me a sign-in link'", async () => {
214
236
  mockGetActiveProviderIds.mockReturnValue(["google", "magic-link"]);
215
237
  await renderPage("/account");
216
- expect(screen.getByTestId("auth-sso-google")).toBeInTheDocument();
217
- expect(screen.getByTestId("auth-email-link-submit")).toHaveTextContent(
238
+ // Google sits under the "or" as a social button, no longer an SSO stack.
239
+ expect(screen.getByTestId("auth-social-google")).toBeInTheDocument();
240
+ expect(screen.queryByTestId("auth-sso-google")).toBeNull();
241
+ expect(screen.getByTestId("auth-submit")).toHaveTextContent(
218
242
  en.signIn.emailLinkSubmit,
219
243
  );
220
244
  expect(en.signIn.emailLinkSubmit).toBe("Email me a sign-in link");
221
- expect(screen.getByTestId("auth-email-link-divider")).toBeInTheDocument();
245
+ // One "or" divider between the primary and Google (#8790 D5).
246
+ expect(screen.getAllByTestId("auth-divider")).toHaveLength(1);
247
+ expect(screen.queryByTestId("auth-email-link-divider")).toBeNull();
222
248
  });
223
249
 
224
250
  it("(h) with magic-link only: the form renders and the not-configured subhead does not", async () => {
225
251
  mockGetActiveProviderIds.mockReturnValue(["magic-link"]);
226
252
  await renderPage();
227
- expect(screen.getByTestId("auth-email-link-form")).toBeInTheDocument();
253
+ expect(screen.getByTestId("auth-email")).toBeInTheDocument();
228
254
  expect(screen.queryByText(en.signIn.notConfiguredSubhead)).toBeNull();
229
- expect(screen.getByText(en.signIn.subhead)).toBeInTheDocument();
255
+ // "Welcome back" stands alone — no subhead (#8790 D3).
256
+ expect(screen.getByText(en.signIn.heading)).toBeInTheDocument();
257
+ expect(en.signIn.heading).toBe("Welcome back");
258
+ expect(screen.queryByTestId("auth-method-switch")).toBeNull();
230
259
  });
231
260
 
232
261
  it("(h) beside the password form when the password method is active too", async () => {
233
262
  mockGetActiveProviderIds.mockReturnValue(["google", "email", "magic-link"]);
234
263
  await renderPage("/account");
264
+ // The sign-in link leads; the password is one quiet switch away, in place
265
+ // (#8790 D4), and the typed email survives the switch.
266
+ expect(screen.queryByTestId("auth-password")).toBeNull();
267
+ expect(screen.getByTestId("auth-submit")).toHaveTextContent(
268
+ en.signIn.emailLinkSubmit,
269
+ );
270
+ const user = userEvent.setup();
271
+ await user.type(screen.getByTestId("auth-email"), "member@example.com");
272
+ await user.click(screen.getByTestId("auth-method-switch"));
235
273
  expect(screen.getByTestId("auth-password")).toBeInTheDocument();
236
- expect(screen.getByTestId("auth-email-link-form")).toBeInTheDocument();
274
+ expect(screen.getByTestId("auth-email")).toHaveValue("member@example.com");
275
+ expect(screen.getByTestId("auth-submit")).toHaveTextContent(
276
+ en.signIn.submit,
277
+ );
278
+ expect(screen.getByTestId("auth-method-switch")).toHaveTextContent(
279
+ en.signIn.useEmailLink,
280
+ );
237
281
  });
238
282
 
239
283
  it.each(["INVALID_TOKEN", "new_user_signup_disabled"])(
@@ -263,3 +307,111 @@ describe("/sign-in page — the email sign-in link (#7435)", () => {
263
307
  expect(screen.queryByTestId("sign-in-link-notice")).toBeNull();
264
308
  });
265
309
  });
310
+
311
+ describe("/sign-in page — the spoke back link (#8790 D2)", () => {
312
+ beforeEach(() => {
313
+ mockClientInfo.mockReset();
314
+ });
315
+
316
+ it("names the spoke from the registry when the continuation opens", async () => {
317
+ mockClientInfo.mockResolvedValue({
318
+ ok: true,
319
+ body: {
320
+ clientId: "spoke_1",
321
+ name: "Wendi Henry",
322
+ origin: "https://wendihenry.com",
323
+ },
324
+ });
325
+ const ref = mintContinuation(
326
+ "client_id=spoke_1&response_type=code",
327
+ TEST_SECRET,
328
+ );
329
+ await renderPage(`/api/auth/hub/continue/${ref}`);
330
+ expect(mockClientInfo).toHaveBeenCalledWith("spoke_1");
331
+ const back = screen.getByTestId("auth-back-link");
332
+ expect(back).toHaveAttribute("href", "https://wendihenry.com/account");
333
+ // This suite's next-intl shim returns messages uninterpolated, so the
334
+ // label's shape is what is asserted here; the ICU fill is next-intl's.
335
+ expect(back).toHaveTextContent(/^Back to/);
336
+ });
337
+
338
+ it("renders no back link when the registry does not know the client", async () => {
339
+ mockClientInfo.mockResolvedValue({ ok: false, status: 404 });
340
+ const ref = mintContinuation("client_id=spoke_gone", TEST_SECRET);
341
+ await renderPage(`/api/auth/hub/continue/${ref}`);
342
+ expect(screen.queryByTestId("auth-back-link")).toBeNull();
343
+ });
344
+
345
+ it("renders no back link on a direct visit", async () => {
346
+ await renderPage("/account");
347
+ expect(screen.queryByTestId("auth-back-link")).toBeNull();
348
+ });
349
+
350
+ it("renders no back link, and no error, when the continuation cannot be opened", async () => {
351
+ // A continuation-shaped `next` whose ref is garbage: the secret load or
352
+ // the open throws, and the page degrades to the direct-visit render.
353
+ mockGetActiveProviderIds.mockReturnValue(["google", "magic-link"]);
354
+ await renderPage("/api/auth/hub/continue/not-a-real-continuation");
355
+ expect(screen.queryByTestId("auth-back-link")).toBeNull();
356
+ expect(screen.getByTestId("auth-email")).toBeInTheDocument();
357
+ });
358
+ });
359
+
360
+ describe("/sign-in page — the hinted email (#8757 D9)", () => {
361
+ async function renderWithEmail(email?: string | string[]): Promise<void> {
362
+ const el = await Page({
363
+ params: Promise.resolve({ locale: "en" }),
364
+ searchParams: Promise.resolve({
365
+ next: "/account",
366
+ ...(email === undefined ? {} : { email }),
367
+ }),
368
+ });
369
+ render(el);
370
+ }
371
+
372
+ function emailInputs(): HTMLInputElement[] {
373
+ return Array.from(
374
+ document.querySelectorAll<HTMLInputElement>('input[type="email"]'),
375
+ );
376
+ }
377
+
378
+ beforeEach(() => {
379
+ // One email input on the card (#8790 D4): the sign-in link and the password share it.
380
+ mockGetActiveProviderIds.mockReturnValue(["google", "email", "magic-link"]);
381
+ });
382
+
383
+ it("(i) ?email=a%40b.example → the card's email input has that value", async () => {
384
+ // Next hands the page the decoded value.
385
+ await renderWithEmail("a@b.example");
386
+ const inputs = emailInputs();
387
+ expect(inputs).toHaveLength(1);
388
+ expect(screen.getByTestId("auth-email")).toHaveValue("a@b.example");
389
+ for (const input of inputs) expect(input).toHaveValue("a@b.example");
390
+ // A starting value only — nothing was sent.
391
+ expect(mockEmail).not.toHaveBeenCalled();
392
+ expect(mockSocial).not.toHaveBeenCalled();
393
+ });
394
+
395
+ it.each([
396
+ ["an invalid value (?email=nope)", "nope"],
397
+ [
398
+ "a repeated value (?email=a%40b.example&email=c%40d.example)",
399
+ ["a@b.example", "c@d.example"],
400
+ ],
401
+ ["a value carrying a query separator", "a@b.example&next=/x"],
402
+ ["no parameter", undefined],
403
+ ] as const)(
404
+ "(i) %s → empty email inputs, as today",
405
+ async (_label, email) => {
406
+ await renderWithEmail(
407
+ email === undefined || typeof email === "string" ? email : [...email],
408
+ );
409
+ const inputs = emailInputs();
410
+ expect(inputs).toHaveLength(1);
411
+ for (const input of inputs) {
412
+ expect(input).toHaveValue("");
413
+ expect(input).not.toHaveAttribute("value");
414
+ }
415
+ },
416
+ );
417
+ });
@@ -53,8 +53,10 @@ function props(overrides: Partial<SignInCardProps> = {}): SignInCardProps {
53
53
 
54
54
  async function requestLink(email = "member@example.com"): Promise<void> {
55
55
  const user = userEvent.setup();
56
- await user.type(screen.getByTestId("auth-email-link-input"), email);
57
- await user.click(screen.getByTestId("auth-email-link-submit"));
56
+ // #8790 D4 — one field, one action: the sign-in link is the card's own
57
+ // email field and primary, not a second form.
58
+ await user.type(screen.getByTestId("auth-email"), email);
59
+ await user.click(screen.getByTestId("auth-submit"));
58
60
  }
59
61
 
60
62
  beforeEach(() => {
@@ -89,7 +91,7 @@ describe("SignInCard — email sign-in link (#7435)", () => {
89
91
  expect(back).toHaveAttribute("href", "/sign-in?next=%2Faccount");
90
92
  expect(document.body.textContent).not.toContain("member@example.com");
91
93
  expect(document.body.textContent).not.toContain("Follow the link we just sent");
92
- expect(screen.queryByTestId("auth-email-link-form")).toBeNull();
94
+ expect(screen.queryByTestId("auth-submit")).toBeNull();
93
95
  });
94
96
 
95
97
  it("lands rateLimited on the field for a 429", async () => {
@@ -102,7 +104,7 @@ describe("SignInCard — email sign-in link (#7435)", () => {
102
104
  expect(
103
105
  await screen.findByText(EMAIL_LINK_LABELS.rateLimited),
104
106
  ).toBeInTheDocument();
105
- expect(screen.getByTestId("auth-email-link-form")).toBeInTheDocument();
107
+ expect(screen.getByTestId("auth-submit")).toBeInTheDocument();
106
108
  });
107
109
 
108
110
  it("lands failed on the field for any other error", async () => {
@@ -127,9 +129,9 @@ describe("SignInCard — email sign-in link (#7435)", () => {
127
129
  mockFetch.mockReturnValue(new Promise((r) => (resolve = r)));
128
130
  const user = userEvent.setup();
129
131
  render(<SignInCard {...props()} />);
130
- await user.type(screen.getByTestId("auth-email-link-input"), "member@example.com");
131
- await user.click(screen.getByTestId("auth-email-link-submit"));
132
- await user.click(screen.getByTestId("auth-email-link-submit"));
132
+ await user.type(screen.getByTestId("auth-email"), "member@example.com");
133
+ await user.click(screen.getByTestId("auth-submit"));
134
+ await user.click(screen.getByTestId("auth-submit"));
133
135
  expect(mockFetch).toHaveBeenCalledTimes(1);
134
136
  resolve({ data: { status: true }, error: null });
135
137
  expect(await screen.findByText(EMAIL_LINK_LABELS.sentHeading)).toBeInTheDocument();
@@ -144,13 +146,14 @@ describe("SignInCard — email sign-in link (#7435)", () => {
144
146
 
145
147
  it("renders the form alone, without the not-configured subhead, when it is the only method", () => {
146
148
  render(<SignInCard {...props({ providers: [] })} />);
147
- expect(screen.getByTestId("auth-email-link-form")).toBeInTheDocument();
149
+ expect(screen.getByTestId("auth-email")).toBeInTheDocument();
150
+ expect(screen.getByTestId("auth-submit")).toHaveTextContent("Email me a sign-in link");
148
151
  expect(screen.queryByText("Sign-in is not configured.")).toBeNull();
149
152
  });
150
153
 
151
154
  it("renders no email-link form when emailLink is false", () => {
152
155
  render(<SignInCard {...props({ emailLink: false, providers: [] })} />);
153
- expect(screen.queryByTestId("auth-email-link-form")).toBeNull();
156
+ expect(screen.queryByTestId("auth-submit")).toBeNull();
154
157
  expect(screen.getByText("Sign-in is not configured.")).toBeInTheDocument();
155
158
  });
156
159
  });
@@ -11,6 +11,11 @@
11
11
  * fails verification lands back here with Better Auth's `?error=`, which maps
12
12
  * to FIXED copy; the raw value is never rendered.
13
13
  *
14
+ * Hinted email (#8757 D9): a single `?email=` that `accountEmailHintSchema`
15
+ * accepts is the starting value of every email input of the card (an account
16
+ * link from a lead's thank-you or invite email). A refused or repeated value
17
+ * is dropped. Only a field's starting value — it signs no one in.
18
+ *
14
19
  * Since #5963 the page lives in the `(auth)` route group — the URL is
15
20
  * unchanged (a group contributes no path segment) but the frame is
16
21
  * `AuthShell`, not the marketing Nav + Footer. The shell owns the brand mark
@@ -22,6 +27,7 @@
22
27
  */
23
28
 
24
29
  import { localeHref } from "@working-theory/ui";
30
+ import { accountEmailHintSchema } from "@working-theory/validation";
25
31
  import type { Metadata } from "next";
26
32
  import { getTranslations } from "next-intl/server";
27
33
  import type { ReactElement } from "react";
@@ -31,13 +37,34 @@ import {
31
37
  resetSendConfigured,
32
38
  } from "../../../../lib/auth-env";
33
39
 
40
+ import { hubSignIn } from "@working-theory/api-client";
41
+ import { Link, Text } from "@working-theory/ui/primitives";
42
+ import { siteLogo } from "../../../../lib/site-logo";
43
+ import {
44
+ CONTINUE_PATH,
45
+ openContinuation,
46
+ } from "../../../api/auth/hub/continuation";
34
47
  import { resolveNextPath } from "./next-path";
35
48
  import { SignInCard, type SignInProvider } from "./sign-in-card";
36
49
  import { resolveSignInCardText, type SignInCardText } from "./sign-in-text";
37
50
 
38
51
  interface Props {
39
52
  params: Promise<{ locale: string }>;
40
- searchParams: Promise<{ next?: string | string[]; error?: string | string[] }>;
53
+ searchParams: Promise<{
54
+ next?: string | string[];
55
+ error?: string | string[];
56
+ email?: string | string[];
57
+ }>;
58
+ }
59
+
60
+ /**
61
+ * #8757 D9 — the hinted email, read once: a repeated `?email=` (an array) or a
62
+ * value the hint schema refuses is dropped, never echoed.
63
+ */
64
+ function hintedEmail(raw: string | string[] | undefined): string | undefined {
65
+ if (typeof raw !== "string") return undefined;
66
+ const parsed = accountEmailHintSchema.safeParse(raw);
67
+ return parsed.success ? parsed.data : undefined;
41
68
  }
42
69
 
43
70
  /**
@@ -67,12 +94,46 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
67
94
  return { title: t("title"), description: t("description") };
68
95
  }
69
96
 
97
+ /**
98
+ * The spoke a hub sign-in continuation came from (#8790 D2), or `null`: not
99
+ * a continuation, a continuation that does not open (expired, forged), a
100
+ * client id the registry does not know, or no API. The spoke's identity is
101
+ * read from the registry by `client_id` — never from a query parameter.
102
+ */
103
+ async function spokeFromNext(
104
+ next: string,
105
+ ): Promise<{ name: string; origin: string } | null> {
106
+ const prefix = `${CONTINUE_PATH}/`;
107
+ if (!next.startsWith(prefix)) return null;
108
+ let query: string | null;
109
+ try {
110
+ // Lazy: `auth.ts` is Better Auth's server module. Only a continuation
111
+ // needs its secret, so a direct visit — and every test of one — never
112
+ // loads it (the `(authed)` gate takes the same care).
113
+ const { resolveSecret } = await import("../../../../auth");
114
+ query = openContinuation(next.slice(prefix.length), resolveSecret());
115
+ } catch {
116
+ return null;
117
+ }
118
+ if (query === null) return null;
119
+ const clientId = new URLSearchParams(query).get("client_id");
120
+ if (!clientId) return null;
121
+ try {
122
+ const result = await hubSignIn.clientInfo(clientId);
123
+ return result.ok
124
+ ? { name: result.body.name, origin: result.body.origin }
125
+ : null;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
70
131
  export default async function SignInPage({
71
132
  params,
72
133
  searchParams,
73
134
  }: Props): Promise<ReactElement> {
74
135
  const { locale } = await params;
75
- const { next: rawNext, error } = await searchParams;
136
+ const { next: rawNext, error, email: rawEmail } = await searchParams;
76
137
  const text = await resolveSignInCardText(locale);
77
138
 
78
139
  const active = getActiveProviderIds();
@@ -87,9 +148,50 @@ export default async function SignInPage({
87
148
  const signInHref = `${localeHref(locale, "/sign-in")}?next=${encodeURIComponent(next)}`;
88
149
  // Only the email link produces `?error=` here; with it off, today's render.
89
150
  const linkNotice = emailLink ? linkNoticeFor(error, text) : undefined;
151
+ const defaultEmail = hintedEmail(rawEmail);
90
152
 
153
+ const spoke = await spokeFromNext(next);
154
+ // The acknowledgement (#8790 D6) — the hub's own policy pages, through the
155
+ // inline link variant; the card never hardcodes a path.
156
+ // `caption` is the muted footnote role; a default `Text` would re-impose
157
+ // body size and the foreground colour over the card's footnote wrapper.
158
+ const footnote = (
159
+ <Text as="p" variant="caption">
160
+ {text.acknowledgement.lead}{" "}
161
+ <Link variant="inline" href={localeHref(locale, "/terms")}>
162
+ {text.acknowledgement.terms}
163
+ </Link>
164
+ ,{" "}
165
+ <Link variant="inline" href={localeHref(locale, "/privacy")}>
166
+ {text.acknowledgement.privacy}
167
+ </Link>
168
+ , and{" "}
169
+ <Link variant="inline" href={localeHref(locale, "/working-agreement")}>
170
+ {text.acknowledgement.workingAgreement}
171
+ </Link>
172
+ .
173
+ </Text>
174
+ );
91
175
  return (
92
176
  <SignInCard
177
+ logo={siteLogo()}
178
+ {...(spoke
179
+ ? {
180
+ backLink: {
181
+ href: `${spoke.origin}/account`,
182
+ label: text.backTo(spoke.name),
183
+ },
184
+ }
185
+ : {})}
186
+ {...(text.prospect
187
+ ? {
188
+ prospect: {
189
+ ...text.prospect,
190
+ href: localeHref(locale, "/sign-up"),
191
+ },
192
+ }
193
+ : {})}
194
+ footnote={footnote}
93
195
  providers={providers}
94
196
  method={method}
95
197
  next={next}
@@ -109,6 +211,7 @@ export default async function SignInPage({
109
211
  errorCallbackURL={signInHref}
110
212
  signInHref={signInHref}
111
213
  {...(linkNotice !== undefined ? { linkNotice } : {})}
214
+ {...(defaultEmail !== undefined ? { defaultEmail } : {})}
112
215
  emailLinkLabels={text.emailLinkLabels}
113
216
  />
114
217
  );
@@ -45,15 +45,20 @@
45
45
  * the same for an address with and without an account, so a success swaps to
46
46
  * a sent state that never names the address. The gate composes this card too
47
47
  * (`(authed)/sign-in-gate.tsx`).
48
+ *
49
+ * Hinted email (#8757 D9): the page hands in `defaultEmail` (already checked
50
+ * by `accountEmailHintSchema`); it reaches AuthCard as the starting value of
51
+ * every email input. Absent → AuthCard's render is unchanged.
48
52
  */
49
53
 
50
54
  import {
55
+ AuthBackLink,
51
56
  AuthCard,
52
57
  type AuthCardLabels,
53
58
  type AuthSubmitData,
54
59
  } from "@working-theory/ui/blocks";
55
60
  import { Alert, AlertDescription } from "@working-theory/ui/primitives";
56
- import { useRef, useState, type ReactElement } from "react";
61
+ import { useRef, useState, type ReactElement, type ReactNode } from "react";
57
62
 
58
63
  import { authClient, useAuthCardFlow } from "../use-auth-card-flow";
59
64
 
@@ -98,6 +103,16 @@ export interface SignInCardProps {
98
103
  /** Fixed copy for a failed link (`?error=` mapped by the page), shown above the card. */
99
104
  linkNotice?: string;
100
105
  emailLinkLabels: SignInEmailLinkLabels;
106
+ /** #8757 D9 — the validated `?email=` hint, the email inputs' starting value. */
107
+ defaultEmail?: string;
108
+ /** The site's mark, centred in the card at 48px (#8790 D3). */
109
+ logo?: ReactNode;
110
+ /** "← Back to <spoke>" when the visitor came from a registered spoke (#8790 D2). */
111
+ backLink?: { href: string; label: string };
112
+ /** The prospect line a site turns on (#8790 D3). */
113
+ prospect?: { prompt: string; linkLabel: string; href: string };
114
+ /** The acknowledgement sentence(s) at the card's foot (#8790 D6). */
115
+ footnote?: ReactNode;
101
116
  }
102
117
 
103
118
  export function SignInCard({
@@ -114,6 +129,11 @@ export function SignInCard({
114
129
  signInHref,
115
130
  linkNotice,
116
131
  emailLinkLabels,
132
+ defaultEmail,
133
+ logo,
134
+ backLink,
135
+ prospect,
136
+ footnote,
117
137
  }: SignInCardProps): ReactElement {
118
138
  const { errors, providerError, launchProvider, submitPassword } =
119
139
  useAuthCardFlow(next, {
@@ -128,6 +148,12 @@ export function SignInCard({
128
148
 
129
149
  const notConfigured =
130
150
  method === "sso" && providers.length === 0 && !emailLink;
151
+ // One field, one action (#8790 D4): the sign-in link first when it is on,
152
+ // the password as the in-place alternative when the email provider is on.
153
+ const methods: ("magic-link" | "password")[] = [
154
+ ...(emailLink ? (["magic-link"] as const) : []),
155
+ ...(method === "password" ? (["password"] as const) : []),
156
+ ];
131
157
 
132
158
  const providerButtons = providers.map((provider) => ({
133
159
  provider,
@@ -198,6 +224,7 @@ export function SignInCard({
198
224
  * in empty space, first input at 470px" defect the auth route group fixes.
199
225
  */
200
226
  <div className="flex flex-col gap-4" data-testid="sign-in-page">
227
+ {backLink ? <AuthBackLink href={backLink.href} label={backLink.label} /> : null}
201
228
  {method === "sso" && providerError ? (
202
229
  <Alert variant="error" data-testid="sign-in-provider-error">
203
230
  <AlertDescription>{providerError}</AlertDescription>
@@ -212,18 +239,24 @@ export function SignInCard({
212
239
  intent="sign-in"
213
240
  method={method}
214
241
  footer="none"
242
+ {...(logo !== undefined ? { logo } : {})}
215
243
  {...(forgotHref !== undefined ? { forgotHref } : {})}
216
244
  {...(forgot !== undefined ? { forgot } : {})}
217
- {...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
245
+ // "Welcome back" stands alone (#8790 D3): no subhead unless the card
246
+ // is inert and must say why.
247
+ subhead={notConfigured ? notConfiguredSubhead : ""}
248
+ {...(prospect !== undefined ? { prospect } : {})}
249
+ {...(footnote !== undefined ? { footnote } : {})}
218
250
  providers={providerButtons}
219
251
  onSubmit={handleSubmit}
220
252
  errors={
221
253
  emailLinkError ? { ...errors, emailLink: emailLinkError } : errors
222
254
  }
223
255
  labels={labels}
256
+ {...(defaultEmail !== undefined ? { defaultEmail } : {})}
257
+ {...(methods.length > 0 ? { methodSwitch: { methods } } : {})}
224
258
  {...(emailLink
225
259
  ? {
226
- emailLink: "form" as const,
227
260
  onEmailLink: (email: string) => void requestEmailLink(email),
228
261
  }
229
262
  : {})}