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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF
This commit is contained in:
Devon Hudson
2026-10-13 14:02:34 +02:00
committed by Quentin Gliech
co-authored by Claude Opus 5.5
parent b7dd2949d2
commit d975dfd5a4
3 changed files with 71 additions and 0 deletions
+2
View File
@@ -258,6 +258,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 <a>Terms and Conditions</a>",
"username_available": "This username is available",
@@ -211,6 +211,15 @@ export const PasswordRegisterForm: React.FC<{
<ErrorAlert>{t("frontend.register.invite_invalid")}</ErrorAlert>
)}
{/* The user never chose a pinned value, so its errors get their own words */}
{pinnedUsername && !!fields.username?.errors.length && (
<ErrorAlert>{t("frontend.register.pinned_username_error")}</ErrorAlert>
)}
{pinnedEmail && !!fields.email?.errors.length && (
<ErrorAlert>{t("frontend.register.pinned_email_error")}</ErrorAlert>
)}
{formErrors.map((error, index) => (
<div
// biome-ignore lint/suspicious/noArrayIndexKey: the server error list is static
@@ -120,4 +120,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();
});
});
});