diff --git a/src/app/features/settings/account/Account.tsx b/src/app/features/settings/account/Account.tsx
index c4b56e47..d5276fc4 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 00000000..2e394918
--- /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 (
+ }>
+
+
+
+
+
+
+ );
+}
+
+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 (
+ (
+ }>
+
+
+
+
+
+
+ )}
+ >
+ {(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 (
+ }>
+
+
+
+
+
+
+ );
+}
+
+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 00000000..5e91f51f
--- /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];
+};