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
@@ -13,6 +13,17 @@ import type { SignInEmailLinkLabels } from "./sign-in-card";
13
13
 
14
14
  export interface SignInCardText {
15
15
  labels: AuthCardLabels;
16
+ /** "Back to {site}" — the spoke's name goes in (#8790 D2). */
17
+ backTo: (site: string) => string;
18
+ /** The acknowledgement sentence's parts (#8790 D6). */
19
+ acknowledgement: {
20
+ lead: string;
21
+ terms: string;
22
+ privacy: string;
23
+ workingAgreement: string;
24
+ };
25
+ /** The prospect line, when the site's messages carry it (#8790 D3). */
26
+ prospect?: { prompt: string; linkLabel: string };
16
27
  notConfiguredSubhead: string;
17
28
  errorLabels: { signInFailed: string; providerFailed: string };
18
29
  emailLinkLabels: SignInEmailLinkLabels;
@@ -20,6 +31,25 @@ export interface SignInCardText {
20
31
  emailLinkExpired: string;
21
32
  }
22
33
 
34
+ type HasKey = Parameters<Awaited<ReturnType<typeof getTranslations>>["has"]>[0];
35
+ function optional(
36
+ t: Awaited<ReturnType<typeof getTranslations>>,
37
+ key: string,
38
+ ): boolean {
39
+ // A partial `getTranslations` mock (the (authed) gate suite's) carries no
40
+ // `has`; an absent `has` reads as an absent key, never a throw.
41
+ return typeof t.has === "function" && t.has(key as HasKey);
42
+ }
43
+
44
+ function prospect(
45
+ prompt: string,
46
+ linkLabel: string,
47
+ ): { prospect: { prompt: string; linkLabel: string } } | null {
48
+ return prompt.trim() && linkLabel.trim()
49
+ ? { prospect: { prompt, linkLabel } }
50
+ : null;
51
+ }
52
+
23
53
  export async function resolveSignInCardText(
24
54
  locale: string,
25
55
  ): Promise<SignInCardText> {
@@ -27,7 +57,6 @@ export async function resolveSignInCardText(
27
57
  return {
28
58
  labels: {
29
59
  signInHeading: t("heading"),
30
- signInSubhead: t("subhead"),
31
60
  ssoPrefix: t("ssoPrefix"),
32
61
  google: t("google"),
33
62
  github: t("github"),
@@ -38,7 +67,24 @@ export async function resolveSignInCardText(
38
67
  orContinue: t("orContinue"),
39
68
  emailLinkSubmit: t("emailLinkSubmit"),
40
69
  emailLinkDivider: t("emailLinkDivider"),
70
+ useEmailLink: t("useEmailLink"),
71
+ emailPlaceholder: t("emailPlaceholder"),
72
+ },
73
+ backTo: (site) => t("backTo", { site }),
74
+ acknowledgement: {
75
+ lead: t("acknowledgement"),
76
+ terms: t("termsLink"),
77
+ privacy: t("privacyLink"),
78
+ workingAgreement: t("workingAgreementLink"),
41
79
  },
80
+ // The prospect line (#8790 D3) is OFF until a site ADDS both keys to its
81
+ // messages — the shipped catalog carries neither (every leaf must be a
82
+ // non-empty string), so presence is the switch. The keys are optional
83
+ // extensions of the typed catalog, hence the cast on `has`.
84
+ ...(optional(t, "prospectPrompt") && optional(t, "prospectLink")
85
+ ? prospect(t("prospectPrompt" as never), t("prospectLink" as never))
86
+ : {}),
87
+
42
88
  notConfiguredSubhead: t("notConfiguredSubhead"),
43
89
  errorLabels: {
44
90
  signInFailed: t("signInFailed"),
@@ -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 password path also mints the carried-agreement
8
- * cookie (`rememberSignUpAgreementsAction`) before calling Better Auth.
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: { signUpFailed: "Sign-up failed.", providerFailed: "Provider failed." },
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("mints the carried-agreement cookie, then signs up, once every required agreement is ticked", async () => {
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
- it("a failed cookie mint still submits — convenience only", async () => {
141
- mockRememberAgreements.mockRejectedValue(new Error("no secret"));
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(mockSignUpEmail).toHaveBeenCalledTimes(1);
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, with no cookie mint", async () => {
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({ provider: "google", callbackURL: "/account" });
163
- expect(mockRememberAgreements).not.toHaveBeenCalled();
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
- const config = SITE_CONSENT_CONFIG_DEFAULTS;
92
- const agreements = signUpAgreements(config).map((purpose) => ({
93
- id: purpose.id,
94
- required: purpose.required,
95
- statement: resolveConsentStatement(purpose, locale).statement,
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={config.policies}
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 { useState, type MouseEvent, type ReactElement } from "react";
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: { signUpFailed: string; providerFailed: string };
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
- const AGREEMENTS_FIELD_NAME = "agreements";
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.filter((a) => a.required).map((a) => a.id);
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>('[role="checkbox"]');
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. No remember-cookie here — Better Auth's own OAuth
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
- void (async () => {
139
- try {
140
- await rememberSignUpAgreementsAction(data.email, requiredAgreementIds);
141
- } catch {
142
- // Convenience only — see sign-up.actions.ts's file header.
143
- }
144
- // Better Auth requires a `name` on email sign-up; this card collects
145
- // none, so the address doubles as the display name until the person
146
- // sets one.
147
- await submitPassword(
148
- authClient.signUp.email({
149
- email: data.email,
150
- password: data.password ?? "",
151
- name: data.email,
152
- callbackURL: next,
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 — see agreements-prompt.tsx's identical note on `react-hooks/exhaustive-deps`.) */}
182
- <div onClick={handleAgreementsClick} data-testid="sign-up-agreements">
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
- /** Launches an OAuth provider via `signIn.social`. */
51
- launchProvider: (provider: AuthProvider) => Promise<void>;
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 (provider: AuthProvider): Promise<void> => {
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({ provider, callbackURL: next })
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
- expect(screen.getByTestId("auth-sso-google")).toBeInTheDocument();
352
- expect(screen.getByTestId("auth-email-link-submit")).toHaveTextContent(
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-link-form")).toBeInTheDocument();
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-link-form")).not.toBeInTheDocument();
384
+ expect(screen.queryByTestId("auth-email")).not.toBeInTheDocument();
382
385
  expect(screen.getByTestId("auth-panel-provider-google")).toBeInTheDocument();
383
386
  });
384
387
  });