Hide the password fields for a passwordless invite

When the invite code the form resolved is `passwordless`, the password fields
are replaced by a note explaining that a verification code will be sent to the
email address instead, which is also why the email field becomes mandatory.
This commit is contained in:
Quentin Gliech
2026-09-10 18:53:59 +02:00
parent 815cc9c1e5
commit 346d0c30f8
4 changed files with 31 additions and 11 deletions
+1
View File
@@ -255,6 +255,7 @@
"or_separator": "Or",
"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.",
"terms_of_service": "I agree to the <a>Terms and Conditions</a>",
"token_checking": "Checking invite code…",
"token_invalid": "This invite code is invalid or has expired",
+25 -7
View File
@@ -4,7 +4,7 @@
// Please see LICENSE files in the repository root for full details.
import { QueryClient, useQuery } from "@tanstack/react-query";
import { Form, InlineSpinner } from "@vector-im/compound-web";
import { Form, InlineSpinner, Text } from "@vector-im/compound-web";
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import * as v from "valibot";
@@ -129,6 +129,7 @@ const REGISTRATION_TOKEN_QUERY = graphql(`
valid
username
email
passwordless
}
}
`);
@@ -138,6 +139,7 @@ type TokenInfo = {
valid: boolean;
username?: string | null;
email?: string | null;
passwordless: boolean;
};
/** The settled result of the token lookup, rendered in a live region. */
@@ -606,8 +608,16 @@ const PasswordRegisterForm: React.FC<{ data: Data }> = ({ data }) => {
const forcedUsername = tokenInfo?.valid ? tokenInfo.username : undefined;
const forcedEmail = tokenInfo?.valid ? tokenInfo.email : undefined;
// A passwordless token skips password creation entirely; the email address is
// used to send a verification code instead
const passwordless = tokenInfo?.valid === true && tokenInfo.passwordless;
// Show the email field when the server requires one, when the token pins one,
// or when it is needed to send the passwordless verification code
const showEmail =
data.features.password_registration_email_required || !!forcedEmail;
data.features.password_registration_email_required ||
!!forcedEmail ||
passwordless;
// With no provider to pick from there is nothing to choose, so the whole form
// is shown at once
@@ -794,11 +804,19 @@ const PasswordRegisterForm: React.FC<{ data: Data }> = ({ data }) => {
</Form.Field>
)}
<PasswordFields
minimumPasswordComplexity={data.features.minimum_password_complexity}
serverErrors={fields.password?.errors ?? []}
confirmServerErrors={fields.password_confirm?.errors ?? []}
/>
{passwordless ? (
<Text size="sm" className="text-secondary">
{t("frontend.register.passwordless_notice")}
</Text>
) : (
<PasswordFields
minimumPasswordComplexity={
data.features.minimum_password_complexity
}
serverErrors={fields.password?.errors ?? []}
confirmServerErrors={fields.password_confirm?.errors ?? []}
/>
)}
{data.branding.tos_uri && (
<Form.InlineField
+3 -3
View File
@@ -49,7 +49,7 @@ type Documents = {
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
"\n fragment BrowserSessionsOverview_user on User {\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc,
"\n query UsernameAvailable($username: String!) {\n usernameAvailable(username: $username) {\n available\n reason\n violationCodes\n }\n }\n": typeof types.UsernameAvailableDocument,
"\n query RegistrationToken($token: String!) {\n registrationToken(token: $token) {\n valid\n username\n email\n }\n }\n": typeof types.RegistrationTokenDocument,
"\n query RegistrationToken($token: String!) {\n registrationToken(token: $token) {\n valid\n username\n email\n passwordless\n }\n }\n": typeof types.RegistrationTokenDocument,
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": typeof types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
@@ -107,7 +107,7 @@ const documents: Documents = {
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
"\n fragment BrowserSessionsOverview_user on User {\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
"\n query UsernameAvailable($username: String!) {\n usernameAvailable(username: $username) {\n available\n reason\n violationCodes\n }\n }\n": types.UsernameAvailableDocument,
"\n query RegistrationToken($token: String!) {\n registrationToken(token: $token) {\n valid\n username\n email\n }\n }\n": types.RegistrationTokenDocument,
"\n query RegistrationToken($token: String!) {\n registrationToken(token: $token) {\n valid\n username\n email\n passwordless\n }\n }\n": types.RegistrationTokenDocument,
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
@@ -270,7 +270,7 @@ export function graphql(source: "\n query UsernameAvailable($username: String!)
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query RegistrationToken($token: String!) {\n registrationToken(token: $token) {\n valid\n username\n email\n }\n }\n"): typeof import('./graphql').RegistrationTokenDocument;
export function graphql(source: "\n query RegistrationToken($token: String!) {\n registrationToken(token: $token) {\n valid\n username\n email\n passwordless\n }\n }\n"): typeof import('./graphql').RegistrationTokenDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
+2 -1
View File
@@ -361,7 +361,7 @@ export type RegistrationTokenQueryVariables = Exact<{
}>;
export type RegistrationTokenQuery = { registrationToken: { valid: boolean, username: string | null, email: string | null } | null };
export type RegistrationTokenQuery = { registrationToken: { valid: boolean, username: string | null, email: string | null, passwordless: boolean } | null };
export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1084,6 +1084,7 @@ export const RegistrationTokenDocument = new TypedDocumentString(`
valid
username
email
passwordless
}
}
`) as unknown as TypedDocumentString<RegistrationTokenQuery, RegistrationTokenQueryVariables>;