diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx
index 86652d2779..8f320459c3 100644
--- a/src/screens/Settings/AccountSettings.tsx
+++ b/src/screens/Settings/AccountSettings.tsx
@@ -25,6 +25,7 @@ import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/ic
import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash'
import * as Layout from '#/components/Layout'
import {ChangeHandleDialog} from './components/ChangeHandleDialog'
+import {ChangePasswordDialog} from './components/ChangePasswordDialog'
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
import {ExportCarDialog} from './components/ExportCarDialog'
@@ -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) {
+
diff --git a/src/screens/Settings/components/ChangePasswordDialog.tsx b/src/screens/Settings/components/ChangePasswordDialog.tsx
new file mode 100644
index 0000000000..7e3e62eeeb
--- /dev/null
+++ b/src/screens/Settings/components/ChangePasswordDialog.tsx
@@ -0,0 +1,300 @@
+import {useState} from 'react'
+import {useWindowDimensions, 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 {isNative} from '#/platform/detection'
+import {useAgent, useSession} from '#/state/session'
+import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
+import {android, atoms as a, 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
+}) {
+ const {height} = useWindowDimensions()
+
+ return (
+
+
+
+
+ )
+}
+
+function Inner() {
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const agent = useAgent()
+ 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 your 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`Please enter the code you received and the new password you would like to use.`,
+ ),
+ },
+ Done: {
+ title: _(msg`Password changed`),
+ message: _(
+ msg`Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on.`,
+ ),
+ },
+ }
+
+ 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) {
+ if (isNetworkError(e)) {
+ setError(
+ _(
+ msg`Unable to contact your service. Please check your internet connection and try again.`,
+ ),
+ )
+ } else {
+ logger.error('Failed to request password reset', {safeMessage: e})
+ 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) {
+ if (isNetworkError(e)) {
+ setError(
+ _(
+ msg`Unable to contact your service. Please check your internet connection and try again.`,
+ ),
+ )
+ } else if (e?.toString().includes('Token is invalid')) {
+ setError(_(msg`This confirmation code is not valid. Please try again.`))
+ } else {
+ logger.error('Failed to set new password', {safeMessage: e})
+ 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 ? (
+ <>
+
+
+ {isNative && (
+
+ )}
+ >
+ ) : stage === Stages.ChangePassword ? (
+ <>
+
+
+ >
+ ) : stage === Stages.Done ? (
+
+ ) : null}
+
+
+
+
+ )
+}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 9197a66c9d..3ebbd1732d 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -39,17 +39,12 @@ export interface PostLanguagesSettingsModal {
name: 'post-languages-settings'
}
-export interface ChangePasswordModal {
- name: 'change-password'
-}
-
/**
* @deprecated DO NOT ADD NEW MODALS
*/
export type Modal =
// Account
| DeleteAccountModal
- | ChangePasswordModal
// Curation
| ContentLanguagesSettingsModal
diff --git a/src/view/com/modals/ChangePassword.tsx b/src/view/com/modals/ChangePassword.tsx
deleted file mode 100644
index 9b96e7db0f..0000000000
--- a/src/view/com/modals/ChangePassword.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-import {useState} from 'react'
-import {
- ActivityIndicator,
- SafeAreaView,
- StyleSheet,
- TouchableOpacity,
- View,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import * as EmailValidator from 'email-validator'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {cleanError, isNetworkError} from '#/lib/strings/errors'
-import {checkAndFormatResetCode} from '#/lib/strings/password'
-import {colors, s} from '#/lib/styles'
-import {logger} from '#/logger'
-import {isAndroid, isWeb} from '#/platform/detection'
-import {useModalControls} from '#/state/modals'
-import {useAgent, useSession} from '#/state/session'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {Button} from '../util/forms/Button'
-import {Text} from '../util/text/Text'
-import {ScrollView} from './util'
-import {TextInput} from './util'
-
-enum Stages {
- RequestCode,
- ChangePassword,
- Done,
-}
-
-export const snapPoints = isAndroid ? ['90%'] : ['45%']
-
-export function Component() {
- const pal = usePalette('default')
- const {currentAccount} = useSession()
- const agent = useAgent()
- const {_} = useLingui()
- const [stage, setStage] = useState(Stages.RequestCode)
- const [isProcessing, setIsProcessing] = useState(false)
- const [resetCode, setResetCode] = useState('')
- const [newPassword, setNewPassword] = useState('')
- const [error, setError] = useState('')
- const {isMobile} = useWebMediaQueries()
- const {closeModal} = useModalControls()
-
- 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) {
- const errMsg = e.toString()
- 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(errMsg))
- }
- } 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) {
- const errMsg = e.toString()
- 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(errMsg))
- }
- } finally {
- setIsProcessing(false)
- }
- }
-
- const onBlur = () => {
- const formattedCode = checkAndFormatResetCode(resetCode)
- if (!formattedCode) {
- setError(
- _(
- msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
- ),
- )
- return
- }
- setResetCode(formattedCode)
- }
-
- return (
-
-
-
-
-
- {stage !== Stages.Done
- ? _(msg`Change Password`)
- : _(msg`Password Changed`)}
-
-
-
-
- {stage === Stages.RequestCode ? (
-
- If you want to change your password, we will send you a code to
- verify that this is your account.
-
- ) : stage === Stages.ChangePassword ? (
-
- Enter the code you received to change your password.
-
- ) : (
- Your password has been changed successfully!
- )}
-
-
- {stage === Stages.RequestCode && (
-
- setStage(Stages.ChangePassword)}
- accessibilityRole="button"
- accessibilityLabel={_(msg`Go to next`)}
- accessibilityHint={_(msg`Navigates to the next screen`)}>
-
- Already have a code?
-
-
-
- )}
- {stage === Stages.ChangePassword && (
-
-
-
- setError('')}
- onBlur={onBlur}
- accessible={true}
- accessibilityLabel={_(msg`Reset Code`)}
- accessibilityHint=""
- autoCapitalize="none"
- autoCorrect={false}
- autoComplete="off"
- />
-
-
-
-
-
-
- )}
- {error ? (
-
- ) : undefined}
-
-
- {isProcessing ? (
-
-
-
- ) : (
-
- {stage === Stages.RequestCode && (
-
- )}
- {stage === Stages.ChangePassword && (
-
- )}
-
- )}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- justifyContent: 'space-between',
- },
- containerMobile: {
- paddingHorizontal: 18,
- paddingBottom: 35,
- },
- titleSection: {
- paddingTop: isWeb ? 0 : 4,
- paddingBottom: isWeb ? 14 : 10,
- },
- title: {
- textAlign: 'center',
- fontWeight: '600',
- marginBottom: 5,
- },
- error: {
- borderRadius: 6,
- },
- textInput: {
- width: '100%',
- paddingHorizontal: 14,
- paddingVertical: 10,
- fontSize: 16,
- },
- btn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: 32,
- padding: 14,
- backgroundColor: colors.blue3,
- },
- btnContainer: {
- paddingTop: 20,
- },
- group: {
- borderWidth: 1,
- borderRadius: 10,
- marginVertical: 20,
- },
- groupLabel: {
- paddingHorizontal: 20,
- paddingBottom: 5,
- },
- groupContent: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- groupBottom: {
- borderTopWidth: 1,
- },
- groupContentIcon: {
- marginLeft: 10,
- },
-})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index f9afd183e6..c3628f9393 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -7,7 +7,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useModalControls, useModals} from '#/state/modals'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
-import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as DeleteAccountModal from './DeleteAccount'
import * as InviteCodesModal from './InviteCodes'
@@ -64,9 +63,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'post-languages-settings') {
snapPoints = PostLanguagesSettingsModal.snapPoints
element =
- } else if (activeModal?.name === 'change-password') {
- snapPoints = ChangePasswordModal.snapPoints
- element =
} else {
return null
}
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 3eb7443802..08f0e2f85a 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -6,7 +6,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type Modal as ModalIface} from '#/state/modals'
import {useModalControls, useModals} from '#/state/modals'
-import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as DeleteAccountModal from './DeleteAccount'
import * as InviteCodesModal from './InviteCodes'
@@ -62,8 +61,6 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'post-languages-settings') {
element =
- } else if (modal.name === 'change-password') {
- element =
} else {
return null
}