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) { +