From 179f1f9b1d09711852f35f9b819a37cb13b150ab Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 15 Sep 2026 10:29:25 +0200 Subject: [PATCH] Extract the password complexity feedback out of `PasswordCreationDoubleInput` The strength meter, the improvement hints and the too-weak error move to a `PasswordComplexityFeedback` component backed by a `usePasswordComplexity` hook, so that forms which lay out their password fields differently can reuse them. `PasswordCreationDoubleInput` takes the minimum complexity as a plain number instead of a `SiteConfig` fragment; its two callers already query `siteConfig`, so they pass the field directly. --- .../components/PasswordComplexityFeedback.tsx | 49 ++++++++++ .../PasswordCreationDoubleInput.tsx | 93 +++---------------- frontend/src/gql/gql.ts | 18 ++-- frontend/src/gql/graphql.ts | 34 ++----- frontend/src/routes/password.change.index.tsx | 5 +- .../src/routes/password.recovery.index.tsx | 5 +- frontend/src/utils/usePasswordComplexity.ts | 46 +++++++++ 7 files changed, 126 insertions(+), 124 deletions(-) create mode 100644 frontend/src/components/PasswordComplexityFeedback.tsx create mode 100644 frontend/src/utils/usePasswordComplexity.ts diff --git a/frontend/src/components/PasswordComplexityFeedback.tsx b/frontend/src/components/PasswordComplexityFeedback.tsx new file mode 100644 index 000000000..e357ef73c --- /dev/null +++ b/frontend/src/components/PasswordComplexityFeedback.tsx @@ -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 ( + <> + complexity.scoreText} + tint={password === "" ? undefined : TINTS[complexity.score]} + max={4} + value={complexity.score} + /> + + {complexity.improvementsText.map((suggestion) => ( + {suggestion} + ))} + + {complexity.score < minimumPasswordComplexity && ( + true}> + {t("frontend.password_strength.too_weak")} + + )} + + ); +}; + +export default PasswordComplexityFeedback; diff --git a/frontend/src/components/PasswordCreationDoubleInput.tsx b/frontend/src/components/PasswordCreationDoubleInput.tsx index 6724463c5..ea77a078a 100644 --- a/frontend/src/components/PasswordCreationDoubleInput.tsx +++ b/frontend/src/components/PasswordCreationDoubleInput.tsx @@ -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({ - 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; - forceShowNewPasswordInvalid: boolean; + minimumPasswordComplexity: number; + forceShowNewPasswordInvalid?: boolean; }): React.ReactElement { const { t } = useTranslation(); - const { minimumPasswordComplexity } = useFragment( - CONFIG_FRAGMENT, - siteConfig, - ); - - const newPasswordRef = useRef(null); const newPasswordAgainRef = useRef(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 ( <> @@ -89,7 +32,6 @@ export default function PasswordCreationDoubleInput({ newPasswordAgainRef.current?.value && newPasswordAgainRef.current?.reportValidity() @@ -97,24 +39,11 @@ export default function PasswordCreationDoubleInput({ onChange={(e) => setNewPassword(e.target.value)} /> - passwordComplexity.scoreText} - tint={passwordStrengthTint} - max={4} - value={passwordComplexity.score} + - {passwordComplexity.improvementsText.map((suggestion) => ( - {suggestion} - ))} - - {passwordComplexity.score < minimumPasswordComplexity && ( - true}> - {t("frontend.password_strength.too_weak")} - - )} - {t("frontend.errors.field_required")} diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 72047364f..eca477e5d 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -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. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 3d6bdb953..63278096d 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -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; -export const PasswordCreationDoubleInput_SiteConfigFragmentDoc = new TypedDocumentString(` - fragment PasswordCreationDoubleInput_siteConfig on SiteConfig { - id - minimumPasswordComplexity -} - `, {"fragmentName":"PasswordCreationDoubleInput_siteConfig"}) as unknown as TypedDocumentString; 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; +} + `, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString; 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; + `) as unknown as TypedDocumentString; 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; export const AllowCrossSigningResetDocument = new TypedDocumentString(` mutation AllowCrossSigningReset($userId: ID!) { diff --git a/frontend/src/routes/password.change.index.tsx b/frontend/src/routes/password.change.index.tsx index 39a6dce80..573c93c74 100644 --- a/frontend/src/routes/password.change.index.tsx +++ b/frontend/src/routes/password.change.index.tsx @@ -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 { 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({ + 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; +};