diff --git a/src/components/dialogs/ChangePasswordDialog.tsx b/src/components/dialogs/ChangePasswordDialog.tsx
new file mode 100644
index 0000000000..0abd2e3b5b
--- /dev/null
+++ b/src/components/dialogs/ChangePasswordDialog.tsx
@@ -0,0 +1,297 @@
+import {useState} from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import * as EmailValidator from 'email-validator'
+
+import {cleanError, isNetworkError} from '#/lib/strings/errors'
+import {checkAndFormatResetCode} from '#/lib/strings/password'
+import {logger} from '#/logger'
+import {useAgent, useSession} from '#/state/session'
+import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
+import {atoms as a, useBreakpoints, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import * as TextField from '#/components/forms/TextField'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+
+enum Stages {
+ RequestCode = 'RequestCode',
+ ChangePassword = 'ChangePassword',
+ Done = 'Done',
+}
+
+export function ChangePasswordDialog({
+ control,
+}: {
+ control: Dialog.DialogControlProps
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function Inner() {
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const agent = useAgent()
+ const {gtMobile} = useBreakpoints()
+ const control = Dialog.useDialogContext()
+
+ const [stage, setStage] = useState(Stages.RequestCode)
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [resetCode, setResetCode] = useState('')
+ const [newPassword, setNewPassword] = useState('')
+ const [error, setError] = useState('')
+
+ const uiStrings = {
+ RequestCode: {
+ title: _(msg`Change Password`),
+ message: _(
+ msg`If you want to change your password, we will send you a code to verify that this is your account.`,
+ ),
+ },
+ ChangePassword: {
+ title: _(msg`Enter Code`),
+ message: _(msg`Enter the code you received to change your password.`),
+ },
+ Done: {
+ title: _(msg`Password Changed`),
+ message: _(msg`Your password has been changed successfully!`),
+ },
+ }
+
+ const onRequestCode = async () => {
+ if (
+ !currentAccount?.email ||
+ !EmailValidator.validate(currentAccount.email)
+ ) {
+ return setError(_(msg`Your email appears to be invalid.`))
+ }
+
+ setError('')
+ setIsProcessing(true)
+ try {
+ await agent.com.atproto.server.requestPasswordReset({
+ email: currentAccount.email,
+ })
+ setStage(Stages.ChangePassword)
+ } catch (e: any) {
+ logger.warn('Failed to request password reset', {error: e})
+ if (isNetworkError(e)) {
+ setError(
+ _(
+ msg`Unable to contact your service. Please check your Internet connection.`,
+ ),
+ )
+ } else {
+ setError(cleanError(e))
+ }
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ const onChangePassword = async () => {
+ const formattedCode = checkAndFormatResetCode(resetCode)
+ if (!formattedCode) {
+ setError(
+ _(
+ msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
+ ),
+ )
+ return
+ }
+ if (!newPassword) {
+ setError(
+ _(msg`Please enter a password. It must be at least 8 characters long.`),
+ )
+ return
+ }
+ if (newPassword.length < 8) {
+ setError(_(msg`Password must be at least 8 characters long.`))
+ return
+ }
+
+ setError('')
+ setIsProcessing(true)
+ try {
+ await agent.com.atproto.server.resetPassword({
+ token: formattedCode,
+ password: newPassword,
+ })
+ setStage(Stages.Done)
+ } catch (e: any) {
+ logger.warn('Failed to set new password', {error: e})
+ if (isNetworkError(e)) {
+ setError(
+ _(
+ msg`Unable to contact your service. Please check your Internet connection.`,
+ ),
+ )
+ } else {
+ setError(cleanError(e))
+ }
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ const onBlur = () => {
+ const formattedCode = checkAndFormatResetCode(resetCode)
+ if (!formattedCode) {
+ return
+ }
+ setResetCode(formattedCode)
+ }
+
+ return (
+
+
+
+
+
+ {uiStrings[stage].title}
+
+ {error ? (
+
+
+
+ ) : null}
+
+
+ {uiStrings[stage].message}
+
+
+
+ {stage === Stages.ChangePassword && (
+
+
+
+ Confirmation Code
+
+
+
+
+
+
+
+ New Password
+
+
+
+
+
+
+ )}
+
+
+ {stage === Stages.RequestCode ? (
+ <>
+
+ {gtMobile ? (
+
+ ) : (
+
+ )}
+ >
+ ) : stage === Stages.ChangePassword ? (
+ <>
+
+
+ >
+ ) : stage === Stages.Done ? (
+
+ ) : null}
+
+
+
+ )
+}
diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx
index 86652d2779..8c425b12d5 100644
--- a/src/screens/Settings/AccountSettings.tsx
+++ b/src/screens/Settings/AccountSettings.tsx
@@ -10,6 +10,7 @@ import {atoms as a, useTheme} from '#/alf'
import {AgeAssuranceAccountCard} from '#/components/ageAssurance/AgeAssuranceAccountCard'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
+import {ChangePasswordDialog} from '#/components/dialogs/ChangePasswordDialog'
import {
EmailDialogScreenID,
useEmailDialogControl,
@@ -37,6 +38,7 @@ export function AccountSettingsScreen({}: Props) {
const emailDialogControl = useEmailDialogControl()
const birthdayControl = useDialogControl()
const changeHandleControl = useDialogControl()
+ const changePasswordControl = useDialogControl()
const exportCarControl = useDialogControl()
const deactivateAccountControl = useDialogControl()
@@ -117,7 +119,7 @@ export function AccountSettingsScreen({}: Props) {
openModal({name: 'change-password'})}>
+ onPress={() => changePasswordControl.open()}>
Password
@@ -180,6 +182,7 @@ export function AccountSettingsScreen({}: Props) {
+