Extract the password complexity feedback out of the double-password input component (#5978)

This commit is contained in:
Quentin Gliech authored and GitHub committed 2026-09-16 10:14:45 +02:00
commit 899a49f055
7 files changed
+126 -124

No files matched your search

@@ -0,0 +1,49 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
import { Form, Progress } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { usePasswordComplexity } from "../utils/usePasswordComplexity";
const TINTS = ["red", "red", "orange", "lime", "green"] as const;
/**
* Strength meter, improvement hints and too-weak error for a new password.
* Renders as a set of `Form.Field` children, so it must be used inside one.
*/
const PasswordComplexityFeedback: React.FC<{
password: string;
minimumPasswordComplexity: number;
}> = ({ password, minimumPasswordComplexity }) => {
const { t } = useTranslation();
const complexity = usePasswordComplexity(password);
return (
<>
<Progress
size="sm"
getValueLabel={() => complexity.scoreText}
tint={password === "" ? undefined : TINTS[complexity.score]}
max={4}
value={complexity.score}
/>
{complexity.improvementsText.map((suggestion) => (
<Form.HelpMessage key={suggestion}>{suggestion}</Form.HelpMessage>
))}
{complexity.score < minimumPasswordComplexity && (
<Form.ErrorMessage match={() => true}>
{t("frontend.password_strength.too_weak")}
</Form.ErrorMessage>
)}
</>
);
};
export default PasswordComplexityFeedback;
@@ -1,84 +1,27 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
import { Form, Progress } from "@vector-im/compound-web";
import { useDeferredValue, useEffect, useRef, useState } from "react";
import { Form } from "@vector-im/compound-web";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../gql";
import type { PasswordComplexity } from "../utils/password_complexity";
const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
id
minimumPasswordComplexity
}
`);
// This will load the password complexity module lazily,
// so that it doesn't block the initial render and can be code-split
const loadPromise = import("../utils/password_complexity").then(
({ estimatePasswordComplexity }) => estimatePasswordComplexity,
);
const usePasswordComplexity = (password: string): PasswordComplexity => {
const { t } = useTranslation();
const [result, setResult] = useState<PasswordComplexity>({
score: 0,
scoreText: t("frontend.password_strength.placeholder"),
improvementsText: [],
});
const deferredPassword = useDeferredValue(password);
useEffect(() => {
if (deferredPassword === "") {
setResult({
score: 0,
scoreText: t("frontend.password_strength.placeholder"),
improvementsText: [],
});
} else {
loadPromise
.then((estimatePasswordComplexity) =>
estimatePasswordComplexity(deferredPassword, t),
)
.then((response) => setResult(response));
}
}, [deferredPassword, t]);
return result;
};
import PasswordComplexityFeedback from "./PasswordComplexityFeedback";
export default function PasswordCreationDoubleInput({
siteConfig,
forceShowNewPasswordInvalid,
minimumPasswordComplexity,
forceShowNewPasswordInvalid = false,
}: {
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
forceShowNewPasswordInvalid: boolean;
minimumPasswordComplexity: number;
forceShowNewPasswordInvalid?: boolean;
}): React.ReactElement {
const { t } = useTranslation();
const { minimumPasswordComplexity } = useFragment(
CONFIG_FRAGMENT,
siteConfig,
);
const newPasswordRef = useRef<HTMLInputElement>(null);
const newPasswordAgainRef = useRef<HTMLInputElement>(null);
const [newPassword, setNewPassword] = useState("");
const passwordComplexity = usePasswordComplexity(newPassword);
let passwordStrengthTint: "red" | "orange" | "lime" | "green" | undefined;
if (newPassword === "") {
passwordStrengthTint = undefined;
} else {
passwordStrengthTint = (["red", "red", "orange", "lime", "green"] as const)[
passwordComplexity.score
];
}
return (
<>
<Form.Field name="new_password">
@@ -89,7 +32,6 @@ export default function PasswordCreationDoubleInput({
<Form.PasswordControl
required
autoComplete="new-password"
ref={newPasswordRef}
onBlur={() =>
newPasswordAgainRef.current?.value &&
newPasswordAgainRef.current?.reportValidity()
@@ -97,24 +39,11 @@ export default function PasswordCreationDoubleInput({
onChange={(e) => setNewPassword(e.target.value)}
/>
<Progress
size="sm"
getValueLabel={() => passwordComplexity.scoreText}
tint={passwordStrengthTint}
max={4}
value={passwordComplexity.score}
<PasswordComplexityFeedback
password={newPassword}
minimumPasswordComplexity={minimumPasswordComplexity}
/>
{passwordComplexity.improvementsText.map((suggestion) => (
<Form.HelpMessage key={suggestion}>{suggestion}</Form.HelpMessage>
))}
{passwordComplexity.score < minimumPasswordComplexity && (
<Form.ErrorMessage match={() => true}>
{t("frontend.password_strength.too_weak")}
</Form.ErrorMessage>
)}
<Form.ErrorMessage match="valueMissing">
{t("frontend.errors.field_required")}
</Form.ErrorMessage>
+6 -12
View File
@@ -25,7 +25,6 @@ type Documents = {
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc,
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument,
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc,
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": typeof types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
"\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": typeof types.EndBrowserSessionButton_SessionFragmentDoc,
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": typeof types.EndBrowserSessionDocument,
"\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.EndCompatSessionButton_SessionFragmentDoc,
@@ -62,11 +61,11 @@ type Documents = {
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n": typeof types.DoVerifyEmailDocument,
"\n mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n": typeof types.ResendEmailAuthenticationCodeDocument,
"\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n": typeof types.ChangePasswordDocument,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": typeof types.PasswordChangeDocument,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n minimumPasswordComplexity\n }\n }\n": typeof types.PasswordChangeDocument,
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": typeof types.RecoverPasswordDocument,
"\n mutation ResendRecoveryEmail($ticket: String!) {\n resendRecoveryEmail(input: { ticket: $ticket }) {\n status\n progressUrl\n }\n }\n": typeof types.ResendRecoveryEmailDocument,
"\n fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {\n username\n email\n }\n": typeof types.RecoverPassword_UserRecoveryTicketFragmentDoc,
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": typeof types.RecoverPassword_SiteConfigFragmentDoc,
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n minimumPasswordComplexity\n }\n": typeof types.RecoverPassword_SiteConfigFragmentDoc,
"\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": typeof types.PasswordRecoveryDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": typeof types.AllowCrossSigningResetDocument,
"\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": typeof types.SessionDetailDocument,
@@ -82,7 +81,6 @@ const documents: Documents = {
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc,
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument,
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc,
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
"\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": types.EndBrowserSessionButton_SessionFragmentDoc,
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": types.EndBrowserSessionDocument,
"\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.EndCompatSessionButton_SessionFragmentDoc,
@@ -119,11 +117,11 @@ const documents: Documents = {
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n": types.DoVerifyEmailDocument,
"\n mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n": types.ResendEmailAuthenticationCodeDocument,
"\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n": types.ChangePasswordDocument,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeDocument,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n minimumPasswordComplexity\n }\n }\n": types.PasswordChangeDocument,
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": types.RecoverPasswordDocument,
"\n mutation ResendRecoveryEmail($ticket: String!) {\n resendRecoveryEmail(input: { ticket: $ticket }) {\n status\n progressUrl\n }\n }\n": types.ResendRecoveryEmailDocument,
"\n fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {\n username\n email\n }\n": types.RecoverPassword_UserRecoveryTicketFragmentDoc,
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": types.RecoverPassword_SiteConfigFragmentDoc,
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n minimumPasswordComplexity\n }\n": types.RecoverPassword_SiteConfigFragmentDoc,
"\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": types.PasswordRecoveryDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument,
"\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument,
@@ -169,10 +167,6 @@ export function graphql(source: "\n query Footer {\n siteConfig {\n id\
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n"): typeof import('./graphql').PasswordCreationDoubleInput_SiteConfigFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -320,7 +314,7 @@ export function graphql(source: "\n mutation ChangePassword(\n $userId: ID!\
/**
* 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 PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): typeof import('./graphql').PasswordChangeDocument;
export function graphql(source: "\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n minimumPasswordComplexity\n }\n }\n"): typeof import('./graphql').PasswordChangeDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -336,7 +330,7 @@ export function graphql(source: "\n fragment RecoverPassword_userRecoveryTicket
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n"): typeof import('./graphql').RecoverPassword_SiteConfigFragmentDoc;
export function graphql(source: "\n fragment RecoverPassword_siteConfig on SiteConfig {\n minimumPasswordComplexity\n }\n"): typeof import('./graphql').RecoverPassword_SiteConfigFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
+8 -26
View File
@@ -224,8 +224,6 @@ export type OAuth2Session_SessionFragment = (
& { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } }
) & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' };
export type PasswordCreationDoubleInput_SiteConfigFragment = { id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' };
export type EndBrowserSessionButton_SessionFragment = { id: string, userAgent: { name: string | null, os: string | null, model: string | null, deviceType: DeviceType } | null } & { ' $fragmentName'?: 'EndBrowserSessionButton_SessionFragment' };
export type EndBrowserSessionMutationVariables = Exact<{
@@ -488,7 +486,7 @@ export type PasswordChangeQueryVariables = Exact<{ [key: string]: never; }>;
export type PasswordChangeQuery = { viewer:
| { __typename: 'Anonymous', id: string }
| { __typename: 'User', id: string }
, siteConfig: { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } } };
, siteConfig: { minimumPasswordComplexity: number } };
export type RecoverPasswordMutationVariables = Exact<{
ticket: string;
@@ -507,7 +505,7 @@ export type ResendRecoveryEmailMutation = { resendRecoveryEmail: { status: Resen
export type RecoverPassword_UserRecoveryTicketFragment = { username: string, email: string } & { ' $fragmentName'?: 'RecoverPassword_UserRecoveryTicketFragment' };
export type RecoverPassword_SiteConfigFragment = { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } } & { ' $fragmentName'?: 'RecoverPassword_SiteConfigFragment' };
export type RecoverPassword_SiteConfigFragment = { minimumPasswordComplexity: number } & { ' $fragmentName'?: 'RecoverPassword_SiteConfigFragment' };
export type PasswordRecoveryQueryVariables = Exact<{
ticket: string;
@@ -914,20 +912,11 @@ export const RecoverPassword_UserRecoveryTicketFragmentDoc = new TypedDocumentSt
email
}
`, {"fragmentName":"RecoverPassword_userRecoveryTicket"}) as unknown as TypedDocumentString<RecoverPassword_UserRecoveryTicketFragment, unknown>;
export const PasswordCreationDoubleInput_SiteConfigFragmentDoc = new TypedDocumentString(`
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
id
minimumPasswordComplexity
}
`, {"fragmentName":"PasswordCreationDoubleInput_siteConfig"}) as unknown as TypedDocumentString<PasswordCreationDoubleInput_SiteConfigFragment, unknown>;
export const RecoverPassword_SiteConfigFragmentDoc = new TypedDocumentString(`
fragment RecoverPassword_siteConfig on SiteConfig {
...PasswordCreationDoubleInput_siteConfig
}
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
id
minimumPasswordComplexity
}`, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString<RecoverPassword_SiteConfigFragment, unknown>;
}
`, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString<RecoverPassword_SiteConfigFragment, unknown>;
export const DeactivateUserDocument = new TypedDocumentString(`
mutation DeactivateUser($hsErase: Boolean!, $password: String) {
deactivateUser(input: {hsErase: $hsErase, password: $password}) {
@@ -1404,13 +1393,10 @@ export const PasswordChangeDocument = new TypedDocumentString(`
}
}
siteConfig {
...PasswordCreationDoubleInput_siteConfig
minimumPasswordComplexity
}
}
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
id
minimumPasswordComplexity
}`) as unknown as TypedDocumentString<PasswordChangeQuery, PasswordChangeQueryVariables>;
`) as unknown as TypedDocumentString<PasswordChangeQuery, PasswordChangeQueryVariables>;
export const RecoverPasswordDocument = new TypedDocumentString(`
mutation RecoverPassword($ticket: String!, $newPassword: String!) {
setPasswordByRecovery(input: {ticket: $ticket, newPassword: $newPassword}) {
@@ -1436,16 +1422,12 @@ export const PasswordRecoveryDocument = new TypedDocumentString(`
...RecoverPassword_userRecoveryTicket
}
}
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
id
minimumPasswordComplexity
}
fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {
fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {
username
email
}
fragment RecoverPassword_siteConfig on SiteConfig {
...PasswordCreationDoubleInput_siteConfig
minimumPasswordComplexity
}`) as unknown as TypedDocumentString<PasswordRecoveryQuery, PasswordRecoveryQueryVariables>;
export const AllowCrossSigningResetDocument = new TypedDocumentString(`
mutation AllowCrossSigningReset($userId: ID!) {
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
@@ -52,7 +53,7 @@ const QUERY = graphql(/* GraphQL */ `
}
siteConfig {
...PasswordCreationDoubleInput_siteConfig
minimumPasswordComplexity
}
}
`);
@@ -183,7 +184,7 @@ function ChangePassword(): React.ReactNode {
<Separator />
<PasswordCreationDoubleInput
siteConfig={siteConfig}
minimumPasswordComplexity={siteConfig.minimumPasswordComplexity}
forceShowNewPasswordInvalid={
(mutation.data &&
mutation.data.status === "INVALID_NEW_PASSWORD") ||
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
@@ -58,7 +59,7 @@ const FRAGMENT = graphql(/* GraphQL */ `
const SITE_CONFIG_FRAGMENT = graphql(/* GraphQL */ `
fragment RecoverPassword_siteConfig on SiteConfig {
...PasswordCreationDoubleInput_siteConfig
minimumPasswordComplexity
}
`);
@@ -292,7 +293,7 @@ const EmailRecovery: React.FC<{
/>
<PasswordCreationDoubleInput
siteConfig={siteConfig}
minimumPasswordComplexity={siteConfig.minimumPasswordComplexity}
forceShowNewPasswordInvalid={
mutation.data?.status === "INVALID_NEW_PASSWORD" || false
}
@@ -0,0 +1,46 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
import { useDeferredValue, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import type { PasswordComplexity } from "./password_complexity";
// This will load the password complexity module lazily,
// so that it doesn't block the initial render and can be code-split
const loadPromise = import("./password_complexity").then(
({ estimatePasswordComplexity }) => estimatePasswordComplexity,
);
/** Score the given password, off the critical path and off the main thread. */
export const usePasswordComplexity = (password: string): PasswordComplexity => {
const { t } = useTranslation();
const [result, setResult] = useState<PasswordComplexity>({
score: 0,
scoreText: t("frontend.password_strength.placeholder"),
improvementsText: [],
});
const deferredPassword = useDeferredValue(password);
useEffect(() => {
if (deferredPassword === "") {
setResult({
score: 0,
scoreText: t("frontend.password_strength.placeholder"),
improvementsText: [],
});
} else {
loadPromise
.then((estimatePasswordComplexity) =>
estimatePasswordComplexity(deferredPassword, t),
)
.then((response) => setResult(response));
}
}, [deferredPassword, t]);
return result;
};