From 4c3a6221e26b2a3e75fb7d99ac3dd0c55ec545e2 Mon Sep 17 00:00:00 2001 From: Thomas Q Date: Thu, 7 Aug 2025 10:29:26 -0300 Subject: [PATCH 1/9] Add `Change Password` section to account settings --- src/app/features/settings/account/Account.tsx | 2 + .../settings/account/ChangePassword.tsx | 347 ++++++++++++++++++ src/app/utils/changePassword.ts | 42 +++ 3 files changed, 391 insertions(+) create mode 100644 src/app/features/settings/account/ChangePassword.tsx create mode 100644 src/app/utils/changePassword.ts diff --git a/src/app/features/settings/account/Account.tsx b/src/app/features/settings/account/Account.tsx index c4b56e4754..d5276fc44e 100644 --- a/src/app/features/settings/account/Account.tsx +++ b/src/app/features/settings/account/Account.tsx @@ -5,6 +5,7 @@ import { MatrixId } from './MatrixId'; import { Profile } from './Profile'; import { ContactInformation } from './ContactInfo'; import { IgnoredUserList } from './IgnoredUserList'; +import { ChangePassword } from './ChangePassword'; type AccountProps = { requestClose: () => void; @@ -33,6 +34,7 @@ export function Account({ requestClose }: AccountProps) { + diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx new file mode 100644 index 0000000000..2e394918a4 --- /dev/null +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -0,0 +1,347 @@ +import React, { FormEventHandler, useCallback, useState, useEffect } from 'react'; +import { + Box, + Text, + Button, + Overlay, + OverlayBackdrop, + OverlayCenter, + Dialog, + Header, + Icon, + IconButton, + Icons, + config, + Spinner, + color, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; +import AuthDict from 'matrix-js-sdk'; +import { SequenceCard } from '../../../components/sequence-card'; +import { SequenceCardStyle } from '../styles.css'; +import { SettingTile } from '../../../components/setting-tile'; +import { PasswordInput } from '../../../components/password-input'; +import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA'; +import { changePassword, ChangePasswordResult } from '../../../utils/changePassword'; +import { useCapabilities } from '../../../hooks/useCapabilities'; + +function ChangePasswordSuccess({ onClose }: { onClose: () => void }) { + return ( + }> + + + +
+ + Password Changed + + + + +
+ + + + Your password has been successfully changed. Your other devices may need to be + re-verified. + + + + +
+
+
+
+ ); +} + +type ChangePasswordFormProps = { + onCancel: () => void; + onSuccess: () => void; +}; + +function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { + const mx = useMatrixClient(); + const [formData, setFormData] = useState<{ newPassword: string; logoutDevices: boolean } | null>( + null + ); + + const [changePasswordState, handleChangePassword] = useAsyncCallback< + ChangePasswordResult, + Error, + [AuthDict | undefined, string, boolean] + >( + useCallback( + async (authDict, newPassword, logoutDevices) => + changePassword(mx, authDict, newPassword, logoutDevices), + [mx] + ) + ); + + const [ongoingAuthData, changePasswordResult] = + changePasswordState.status === AsyncStatus.Success + ? changePasswordState.data + : [undefined, undefined]; + + const handleFormSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + + const formDataObj = new FormData(evt.currentTarget); + const newPassword = formDataObj.get('newPassword') as string; + const confirmPassword = formDataObj.get('confirmPassword') as string; + const logoutDevices = formDataObj.get('logoutDevices') === 'on'; + + if (!newPassword || !confirmPassword) return; + if (newPassword !== confirmPassword) return; + + // Store form data for UIA completion + const formState = { newPassword, logoutDevices }; + setFormData(formState); + + // Just call the async callback - don't handle the result here + // The component state will automatically update and handle UIA vs success + handleChangePassword(undefined, newPassword, logoutDevices); + }; + + // Handle successful completion + useEffect(() => { + if (changePasswordResult && !ongoingAuthData) { + onSuccess(); + } + }, [changePasswordResult, ongoingAuthData, onSuccess]); + + // Don't show success dialog in this component - let parent handle it + if (changePasswordResult && !ongoingAuthData) { + return null; // Success state handled by parent component + } + + // Show UIA flow if we have auth data + if (ongoingAuthData) { + return ( + ( + }> + + + + + + This server requires authentication methods that are not supported by this + client. + + + + + + + + )} + > + {(ongoingFlow) => ( + { + if (formData) { + handleChangePassword(authDict, formData.newPassword, formData.logoutDevices); + } else { + onCancel(); + } + }} + onCancel={onCancel} + /> + )} + + ); + } + + const isLoading = changePasswordState.status === AsyncStatus.Loading; + const error = + changePasswordState.status === AsyncStatus.Error ? changePasswordState.error : undefined; + + return ( + }> + + + +
+ + Change Password + + + + +
+ + + + Enter your new password. You may need to re-verify your other devices after + changing your password. + + + + {(match, doMatch, passRef, confPassRef) => ( + <> + + New Password + + + + Confirm New Password + + + + )} + + + + + + + Sign out all other devices + + + + Recommended for security. Unchecking this may leave your other devices logged + in. + + + + {error && ( + + + + Failed to change password: {error.message} + + + )} + + + + + + + +
+
+
+
+ ); +} + +export function ChangePassword() { + const [showDialog, setShowDialog] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const capabilities = useCapabilities(); + + // Check if password change is disabled by server capabilities + const disableChangePassword = capabilities['m.change_password']?.enabled === false; + + const handleOpenDialog = () => setShowDialog(true); + const handleCloseDialog = () => { + setShowDialog(false); + setShowSuccess(false); + }; + const handleSuccess = () => { + setShowDialog(false); + setShowSuccess(true); + }; + + return ( + <> + + Password + + + Change + + } + /> + + + + {showDialog && } + + {showSuccess && } + + ); +} diff --git a/src/app/utils/changePassword.ts b/src/app/utils/changePassword.ts new file mode 100644 index 0000000000..5e91f51fa4 --- /dev/null +++ b/src/app/utils/changePassword.ts @@ -0,0 +1,42 @@ +import to from 'await-to-js'; +import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk'; + +export type ChangePasswordResponse = Record; +export type ChangePasswordResult = [IAuthData, undefined] | [undefined, ChangePasswordResponse]; + +/** + * Change the user's password using the Matrix password change API + * @param mx Matrix client instance + * @param authDict Authentication dictionary for UIA (undefined for initial request) + * @param newPassword The new password to set + * @param logoutDevices Whether to logout other devices (defaults to true for security) + * @returns Tuple with either auth data (for UIA continuation) or success response + */ +export const changePassword = async ( + mx: MatrixClient, + authDict: AuthDict | undefined, + newPassword: string, + logoutDevices = true +): Promise => { + + // For the initial request, pass undefined instead of null + // This ensures the auth field is omitted from the request body + const [err, res] = await to( + mx.setPassword(authDict, newPassword, logoutDevices) + ); + + if (err) { + console.log('Password change error:', err.httpStatus, err.data); + // If we get a 401, it means we need to perform UIA + if (err.httpStatus === 401) { + const authData = err.data as IAuthData; + return [authData, undefined]; + } + // Any other error should be thrown + throw err; + } + + console.log('Password change successful:', res); + // Success case - return empty response + return [undefined, res]; +}; From 4eb1efc3994ca91f6ed01f6bd127c8d1a43009c5 Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 18:18:48 +0000 Subject: [PATCH 2/9] Change message and design to better fit in with other parts of cinny's design, hide the section if the option is disabled --- .../settings/account/ChangePassword.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index 2e394918a4..8bbeabecde 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -268,11 +268,6 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { - } /> From 48e3e85ca3ce46a7aefcadbd0848562d19a0ab24 Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 18:21:22 +0000 Subject: [PATCH 3/9] Apply PR review feedback --- .../settings/account/ChangePassword.tsx | 44 ++++++++++++++++++- src/app/utils/changePassword.ts | 39 ---------------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index 8bbeabecde..602924a0e8 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -1,3 +1,5 @@ +import to from 'await-to-js'; +import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk'; import React, { FormEventHandler, useCallback, useState, useEffect } from 'react'; import { Box, @@ -25,9 +27,49 @@ import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA'; -import { changePassword, ChangePasswordResult } from '../../../utils/changePassword'; import { useCapabilities } from '../../../hooks/useCapabilities'; +type ChangePasswordResponse = Record; +type ChangePasswordResult = [IAuthData, undefined] | [undefined, ChangePasswordResponse]; + +/** + * Change the user's password using the Matrix password change API + * @param mx Matrix client instance + * @param authDict Authentication dictionary for UIA (undefined for initial request) + * @param newPassword The new password to set + * @param logoutDevices Whether to logout other devices (defaults to true for security) + * @returns Tuple with either auth data (for UIA continuation) or success response + */ +const changePassword = async ( + mx: MatrixClient, + authDict: AuthDict | undefined, + newPassword: string, + logoutDevices = true +): Promise => { + + // For the initial request, pass undefined instead of null + // This ensures the auth field is omitted from the request body + const [err, res] = await to( + mx.setPassword(authDict, newPassword, logoutDevices) + ); + + if (err) { + console.log('Password change error:', err.httpStatus, err.data); + // If we get a 401, it means we need to perform UIA + if (err.httpStatus === 401) { + const authData = err.data as IAuthData; + return [authData, undefined]; + } + // Any other error should be thrown + throw err; + } + + console.log('Password change successful:', res); + // Success case - return empty response + return [undefined, res]; +}; + + function ChangePasswordSuccess({ onClose }: { onClose: () => void }) { return ( }> diff --git a/src/app/utils/changePassword.ts b/src/app/utils/changePassword.ts index 5e91f51fa4..b28b04f643 100644 --- a/src/app/utils/changePassword.ts +++ b/src/app/utils/changePassword.ts @@ -1,42 +1,3 @@ -import to from 'await-to-js'; -import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk'; -export type ChangePasswordResponse = Record; -export type ChangePasswordResult = [IAuthData, undefined] | [undefined, ChangePasswordResponse]; -/** - * Change the user's password using the Matrix password change API - * @param mx Matrix client instance - * @param authDict Authentication dictionary for UIA (undefined for initial request) - * @param newPassword The new password to set - * @param logoutDevices Whether to logout other devices (defaults to true for security) - * @returns Tuple with either auth data (for UIA continuation) or success response - */ -export const changePassword = async ( - mx: MatrixClient, - authDict: AuthDict | undefined, - newPassword: string, - logoutDevices = true -): Promise => { - // For the initial request, pass undefined instead of null - // This ensures the auth field is omitted from the request body - const [err, res] = await to( - mx.setPassword(authDict, newPassword, logoutDevices) - ); - - if (err) { - console.log('Password change error:', err.httpStatus, err.data); - // If we get a 401, it means we need to perform UIA - if (err.httpStatus === 401) { - const authData = err.data as IAuthData; - return [authData, undefined]; - } - // Any other error should be thrown - throw err; - } - - console.log('Password change successful:', res); - // Success case - return empty response - return [undefined, res]; -}; From 1ec0511cd99817f967ecb41fd9101200f582359a Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 19:03:52 +0000 Subject: [PATCH 4/9] Require current password when changing password --- .../settings/account/ChangePassword.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index 602924a0e8..1500036e31 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -1,8 +1,8 @@ import to from 'await-to-js'; -import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk'; import React, { FormEventHandler, useCallback, useState, useEffect } from 'react'; import { Box, + Checkbox, Text, Button, Overlay, @@ -18,7 +18,7 @@ import { color, } from 'folds'; import FocusTrap from 'focus-trap-react'; -import AuthDict from 'matrix-js-sdk'; +import { PasswordDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; @@ -54,7 +54,7 @@ const changePassword = async ( ); if (err) { - console.log('Password change error:', err.httpStatus, err.data); + console.error('Password change error:', err.httpStatus, err.data); // If we get a 401, it means we need to perform UIA if (err.httpStatus === 401) { const authData = err.data as IAuthData; @@ -64,8 +64,6 @@ const changePassword = async ( throw err; } - console.log('Password change successful:', res); - // Success case - return empty response return [undefined, res]; }; @@ -128,7 +126,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { const [changePasswordState, handleChangePassword] = useAsyncCallback< ChangePasswordResult, Error, - [AuthDict | undefined, string, boolean] + [AuthDict, string, boolean] >( useCallback( async (authDict, newPassword, logoutDevices) => @@ -146,12 +144,14 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { evt.preventDefault(); const formDataObj = new FormData(evt.currentTarget); + const currentPassword = formDataObj.get('currentPassword') as string; const newPassword = formDataObj.get('newPassword') as string; const confirmPassword = formDataObj.get('confirmPassword') as string; const logoutDevices = formDataObj.get('logoutDevices') === 'on'; - if (!newPassword || !confirmPassword) return; - if (newPassword !== confirmPassword) return; + if (!currentPassword || !newPassword || !confirmPassword || newPassword !== confirmPassword) { + return; + } // Store form data for UIA completion const formState = { newPassword, logoutDevices }; @@ -159,7 +159,12 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { // Just call the async callback - don't handle the result here // The component state will automatically update and handle UIA vs success - handleChangePassword(undefined, newPassword, logoutDevices); + handleChangePassword({ + identifier: mx.getUserId()!, + password: currentPassword, + session: mx.getSessionId()!, + type: 'Password' + }, newPassword, logoutDevices); }; // Handle successful completion From 86dced8b82026bab1f81014dee47bbd2b431208b Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 19:04:34 +0000 Subject: [PATCH 5/9] Keep the section but display a message to contact your administrator if the homeserver doesn't allow changing password --- .../settings/account/ChangePassword.tsx | 106 +++++++++++------- 1 file changed, 67 insertions(+), 39 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index 1500036e31..d112b158d3 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -256,52 +256,51 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { > - Enter your new password. You may need to re-verify your other devices after - changing your password. + Enter your current password and a new password. - {(match, doMatch, passRef, confPassRef) => ( - <> - - New Password - - - - Confirm New Password - - - + {(match, doMatch, currPassRef, passRef, confPassRef) => ( + + Current Password + + New Password + + Confirm New Password + + )} - - - Sign out all other devices + + + Logout other devices - - Recommended for security. Unchecking this may leave your other devices logged - in. - {error && ( @@ -318,7 +317,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { @@ -349,7 +348,36 @@ export function ChangePassword() { }; if (disableChangePassword) { - return (<>); + return ( + <> + + Account Security + + + Change + + } + /> + + + + ); } return ( @@ -374,7 +402,7 @@ export function ChangePassword() { radii="300" onClick={handleOpenDialog} > - Edit + Change } /> From 889debde6505b2487bfab66855b6ba402a9a740e Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 19:53:35 +0000 Subject: [PATCH 6/9] Remove empty file --- src/app/utils/changePassword.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 src/app/utils/changePassword.ts diff --git a/src/app/utils/changePassword.ts b/src/app/utils/changePassword.ts deleted file mode 100644 index b28b04f643..0000000000 --- a/src/app/utils/changePassword.ts +++ /dev/null @@ -1,3 +0,0 @@ - - - From 0215058bf964ebf73992f53a17c18c1cf3e407c1 Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 21:07:35 +0000 Subject: [PATCH 7/9] Remove the extra "current password" field because this is handled the UIAFlow stuff --- .../settings/account/ChangePassword.tsx | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index d112b158d3..ea38034a8b 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -126,7 +126,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { const [changePasswordState, handleChangePassword] = useAsyncCallback< ChangePasswordResult, Error, - [AuthDict, string, boolean] + [AuthDict | null, string, boolean] >( useCallback( async (authDict, newPassword, logoutDevices) => @@ -144,12 +144,11 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { evt.preventDefault(); const formDataObj = new FormData(evt.currentTarget); - const currentPassword = formDataObj.get('currentPassword') as string; const newPassword = formDataObj.get('newPassword') as string; const confirmPassword = formDataObj.get('confirmPassword') as string; const logoutDevices = formDataObj.get('logoutDevices') === 'on'; - if (!currentPassword || !newPassword || !confirmPassword || newPassword !== confirmPassword) { + if (!newPassword || !confirmPassword || newPassword !== confirmPassword) { return; } @@ -159,12 +158,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { // Just call the async callback - don't handle the result here // The component state will automatically update and handle UIA vs success - handleChangePassword({ - identifier: mx.getUserId()!, - password: currentPassword, - session: mx.getSessionId()!, - type: 'Password' - }, newPassword, logoutDevices); + handleChangePassword(null, newPassword, logoutDevices); }; // Handle successful completion @@ -225,8 +219,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { } const isLoading = changePasswordState.status === AsyncStatus.Loading; - const error = - changePasswordState.status === AsyncStatus.Error ? changePasswordState.error : undefined; + const error = changePasswordState.status === AsyncStatus.Error ? changePasswordState.error : undefined; return ( }> @@ -260,17 +253,8 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { - {(match, doMatch, currPassRef, passRef, confPassRef) => ( + {(match, doMatch, passRef, confPassRef) => ( - Current Password - New Password Date: Mon, 9 Mar 2026 21:11:53 +0000 Subject: [PATCH 8/9] Clean up formatting --- .../settings/account/ChangePassword.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index ea38034a8b..a491d815f7 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -31,6 +31,15 @@ import { useCapabilities } from '../../../hooks/useCapabilities'; type ChangePasswordResponse = Record; type ChangePasswordResult = [IAuthData, undefined] | [undefined, ChangePasswordResponse]; +type ChangePasswordFormProps = { + onCancel: () => void; + onSuccess: () => void; +}; + +type ChangePasswordData = { + newPassword: string; + logoutDevices: boolean; +}; /** * Change the user's password using the Matrix password change API @@ -112,16 +121,9 @@ function ChangePasswordSuccess({ onClose }: { onClose: () => void }) { ); } -type ChangePasswordFormProps = { - onCancel: () => void; - onSuccess: () => void; -}; - function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { const mx = useMatrixClient(); - const [formData, setFormData] = useState<{ newPassword: string; logoutDevices: boolean } | null>( - null - ); + const [formData, setFormData] = useState(null); const [changePasswordState, handleChangePassword] = useAsyncCallback< ChangePasswordResult, @@ -153,8 +155,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { } // Store form data for UIA completion - const formState = { newPassword, logoutDevices }; - setFormData(formState); + setFormData({ newPassword, logoutDevices }); // Just call the async callback - don't handle the result here // The component state will automatically update and handle UIA vs success @@ -394,7 +395,6 @@ export function ChangePassword() { {showDialog && } - {showSuccess && } ); From cbcf7c7023c2165b2579554266f9390cbe3052d3 Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 9 Mar 2026 21:27:47 +0000 Subject: [PATCH 9/9] Sort the functions from top to bottom and inline the changePassword sync function --- .../settings/account/ChangePassword.tsx | 337 ++++++++---------- 1 file changed, 158 insertions(+), 179 deletions(-) diff --git a/src/app/features/settings/account/ChangePassword.tsx b/src/app/features/settings/account/ChangePassword.tsx index a491d815f7..80f2388fd7 100644 --- a/src/app/features/settings/account/ChangePassword.tsx +++ b/src/app/features/settings/account/ChangePassword.tsx @@ -41,83 +41,89 @@ type ChangePasswordData = { logoutDevices: boolean; }; -/** - * Change the user's password using the Matrix password change API - * @param mx Matrix client instance - * @param authDict Authentication dictionary for UIA (undefined for initial request) - * @param newPassword The new password to set - * @param logoutDevices Whether to logout other devices (defaults to true for security) - * @returns Tuple with either auth data (for UIA continuation) or success response - */ -const changePassword = async ( - mx: MatrixClient, - authDict: AuthDict | undefined, - newPassword: string, - logoutDevices = true -): Promise => { - - // For the initial request, pass undefined instead of null - // This ensures the auth field is omitted from the request body - const [err, res] = await to( - mx.setPassword(authDict, newPassword, logoutDevices) - ); +export function ChangePassword() { + const [showDialog, setShowDialog] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const capabilities = useCapabilities(); - if (err) { - console.error('Password change error:', err.httpStatus, err.data); - // If we get a 401, it means we need to perform UIA - if (err.httpStatus === 401) { - const authData = err.data as IAuthData; - return [authData, undefined]; - } - // Any other error should be thrown - throw err; - } + // Check if password change is disabled by server capabilities + const disableChangePassword = capabilities['m.change_password']?.enabled === false; - return [undefined, res]; -}; + const handleOpenDialog = () => setShowDialog(true); + const handleCloseDialog = () => { + setShowDialog(false); + setShowSuccess(false); + }; + const handleSuccess = () => { + setShowDialog(false); + setShowSuccess(true); + }; + if (disableChangePassword) { + return ( + <> + + Account Security + + + Change + + } + /> + + + + ); + } -function ChangePasswordSuccess({ onClose }: { onClose: () => void }) { return ( - }> - - - -
- - Password Changed - - - - -
- - - - Your password has been successfully changed. Your other devices may need to be - re-verified. - - - - -
-
-
-
+ } + /> + +
+ + {showDialog && } + {showSuccess && } + ); } @@ -125,16 +131,31 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { const mx = useMatrixClient(); const [formData, setFormData] = useState(null); - const [changePasswordState, handleChangePassword] = useAsyncCallback< + const [changePasswordState, attemptPasswordChange] = useAsyncCallback< ChangePasswordResult, Error, [AuthDict | null, string, boolean] >( - useCallback( - async (authDict, newPassword, logoutDevices) => - changePassword(mx, authDict, newPassword, logoutDevices), - [mx] - ) + useCallback(async (authDict, newPassword, logoutDevices) => { + // For the initial request, pass undefined instead of null + // This ensures the auth field is omitted from the request body + const [err, res] = await to( + mx.setPassword(authDict, newPassword, logoutDevices) + ); + + if (err) { + console.error('Password change error:', err.httpStatus, err.data); + // If we get a 401, it means we need to perform UIA + if (err.httpStatus === 401) { + const authData = err.data as IAuthData; + return [authData, undefined]; + } + // Any other error should be thrown + throw err; + } + + return [undefined, res]; + }, [mx]) ); const [ongoingAuthData, changePasswordResult] = @@ -142,26 +163,6 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { ? changePasswordState.data : [undefined, undefined]; - const handleFormSubmit: FormEventHandler = (evt) => { - evt.preventDefault(); - - const formDataObj = new FormData(evt.currentTarget); - const newPassword = formDataObj.get('newPassword') as string; - const confirmPassword = formDataObj.get('confirmPassword') as string; - const logoutDevices = formDataObj.get('logoutDevices') === 'on'; - - if (!newPassword || !confirmPassword || newPassword !== confirmPassword) { - return; - } - - // Store form data for UIA completion - setFormData({ newPassword, logoutDevices }); - - // Just call the async callback - don't handle the result here - // The component state will automatically update and handle UIA vs success - handleChangePassword(null, newPassword, logoutDevices); - }; - // Handle successful completion useEffect(() => { if (changePasswordResult && !ongoingAuthData) { @@ -207,7 +208,7 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { ongoingFlow={ongoingFlow} action={(authDict) => { if (formData) { - handleChangePassword(authDict, formData.newPassword, formData.logoutDevices); + attemptPasswordChange(authDict, formData.newPassword, formData.logoutDevices); } else { onCancel(); } @@ -221,6 +222,26 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { const isLoading = changePasswordState.status === AsyncStatus.Loading; const error = changePasswordState.status === AsyncStatus.Error ? changePasswordState.error : undefined; + const handleFormSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + + const formDataObj = new FormData(evt.currentTarget); + const newPassword = formDataObj.get('newPassword') as string; + const confirmPassword = formDataObj.get('confirmPassword') as string; + const logoutDevices = formDataObj.get('logoutDevices') === 'on'; + + if (!newPassword || !confirmPassword || newPassword !== confirmPassword) { + return; + } + + // Store form data for UIA completion + setFormData({ newPassword, logoutDevices }); + + // Try to set password without authentication. + // Response will contain authData for UIA to proceed. + // TODO: is there ANY way to do UIA before asking the user for their new password? + attemptPasswordChange(null, newPassword, logoutDevices); + }; return ( }> @@ -314,88 +335,46 @@ function ChangePasswordForm({ onCancel, onSuccess }: ChangePasswordFormProps) { ); } -export function ChangePassword() { - const [showDialog, setShowDialog] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const capabilities = useCapabilities(); - - // Check if password change is disabled by server capabilities - const disableChangePassword = capabilities['m.change_password']?.enabled === false; - - const handleOpenDialog = () => setShowDialog(true); - const handleCloseDialog = () => { - setShowDialog(false); - setShowSuccess(false); - }; - const handleSuccess = () => { - setShowDialog(false); - setShowSuccess(true); - }; - - if (disableChangePassword) { - return ( - <> - - Account Security - - - Change - - } - /> - - - - ); - } - +function ChangePasswordSuccess({ onClose }: { onClose: () => void }) { return ( - <> - - Account Security - - - Change + }> + + + +
+ + Password Changed + + + + +
+ + + + Your password has been successfully changed. Your other devices may need to be + re-verified. + + + - } - /> -
-
- - {showDialog && } - {showSuccess && } - + + + + +
); }