From 4fb61c22df8f6fe27e90909dbf2dafcb50e247f2 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Fri, 2 Oct 2026 22:47:30 +0000 Subject: [PATCH] Show the errors of pinned registration fields above the form The registration form doesn't render a username pinned by the invite, and shows a pinned email address as text, so errors the server reports on either field weren't shown at all: the form just came back. The invitee never chose these values, so "This username is already taken" would mean nothing to them either. Each pinned field with an error now gets its own message above the form, saying the invite can't be used and to let whoever sent it know: a policy which refuses the address or the username would refuse a new invite just the same. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF --- frontend/locales/en.json | 2 + frontend/src/entrypoints/register.test.tsx | 60 ++++++++++++++++++++++ frontend/src/entrypoints/register.tsx | 9 ++++ 3 files changed, 71 insertions(+) diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 06e7627be..840980ab2 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -256,6 +256,8 @@ "password_confirm_label": "Confirm password", "password_label": "Password", "passwordless_notice": "This account doesn't need a password. We'll send a verification code to your email address instead.", + "pinned_email_error": "This invite's email address can't be used to create an account. Let whoever invited you know.", + "pinned_username_error": "The account this invite is for can't be created. Let whoever invited you know.", "send_code": "Send code", "terms_of_service": "I agree to the Terms and Conditions" }, diff --git a/frontend/src/entrypoints/register.test.tsx b/frontend/src/entrypoints/register.test.tsx index 768b04b40..ef65d669a 100644 --- a/frontend/src/entrypoints/register.test.tsx +++ b/frontend/src/entrypoints/register.test.tsx @@ -119,4 +119,64 @@ describe("register island", () => { ).toBeTruthy(); }); }); + + describe("with an invite pinning the username and email", () => { + const invite = { + valid: true, + username: "alice", + email: "alice@example.com", + passwordless: true, + }; + + it("explains an error on the username above the form", async () => { + await mountRegister({ + invite, + form: { + errors: [], + fields: { username: { value: "", errors: [{ kind: "exists" }] } }, + }, + }); + + const alert = screen.getByRole("alert"); + expect(alert).toHaveTextContent( + "The account this invite is for can't be created.", + ); + expect( + alert.compareDocumentPosition( + screen.getByRole("button", { name: "Send code" }), + ) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); + + it("explains an error on the email address above the form", async () => { + await mountRegister({ + invite, + form: { + errors: [], + fields: { + email: { + value: "alice@example.com", + errors: [ + { + kind: "policy", + code: "email-domain-banned", + message: "Email domain is banned", + }, + ], + }, + }, + }, + }); + + const alert = screen.getByRole("alert"); + expect(alert).toHaveTextContent( + "This invite's email address can't be used to create an account.", + ); + expect( + alert.compareDocumentPosition( + screen.getByRole("button", { name: "Send code" }), + ) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); + }); }); diff --git a/frontend/src/entrypoints/register.tsx b/frontend/src/entrypoints/register.tsx index 18dab5845..08eca6087 100644 --- a/frontend/src/entrypoints/register.tsx +++ b/frontend/src/entrypoints/register.tsx @@ -381,6 +381,15 @@ const PasswordRegisterForm: React.FC<{ data: Data }> = ({ data }) => { {t("frontend.register.invite_invalid")} )} + {/* The user never chose a pinned value, so its errors get their own words */} + {pinnedUsername && !!fields.username?.errors.length && ( + {t("frontend.register.pinned_username_error")} + )} + + {pinnedEmail && !!fields.email?.errors.length && ( + {t("frontend.register.pinned_email_error")} + )} + {formErrors.map((error, index) => (