diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/screens/Settings/AccountSettings.tsx | 5 | ||||
-rw-r--r-- | src/screens/Settings/components/ChangePasswordDialog.tsx | 300 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 5 | ||||
-rw-r--r-- | src/view/com/modals/ChangePassword.tsx | 350 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 |
6 files changed, 304 insertions, 363 deletions
diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index 86652d277..8f320459c 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) { <SettingsList.Divider /> <SettingsList.PressableItem label={_(msg`Password`)} - onPress={() => openModal({name: 'change-password'})}> + onPress={() => changePasswordControl.open()}> <SettingsList.ItemIcon icon={LockIcon} /> <SettingsList.ItemText> <Trans>Password</Trans> @@ -180,6 +182,7 @@ export function AccountSettingsScreen({}: Props) { <BirthDateSettingsDialog control={birthdayControl} /> <ChangeHandleDialog control={changeHandleControl} /> + <ChangePasswordDialog control={changePasswordControl} /> <ExportCarDialog control={exportCarControl} /> <DeactivateAccountDialog control={deactivateAccountControl} /> </Layout.Screen> diff --git a/src/screens/Settings/components/ChangePasswordDialog.tsx b/src/screens/Settings/components/ChangePasswordDialog.tsx new file mode 100644 index 000000000..7e3e62eee --- /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 ( + <Dialog.Outer + control={control} + nativeOptions={android({minHeight: height / 2})}> + <Dialog.Handle /> + <Inner /> + </Dialog.Outer> + ) +} + +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 ( + <Dialog.ScrollableInner + label={_(msg`Change password dialog`)} + style={web({maxWidth: 400})}> + <View style={[a.gap_xl]}> + <View style={[a.gap_sm]}> + <Text style={[a.font_heavy, a.text_2xl]}> + {uiStrings[stage].title} + </Text> + {error ? ( + <View style={[a.rounded_sm, a.overflow_hidden]}> + <ErrorMessage message={error} /> + </View> + ) : null} + + <Text style={[a.text_md, a.leading_snug]}> + {uiStrings[stage].message} + </Text> + </View> + + {stage === Stages.ChangePassword && ( + <View style={[a.gap_md]}> + <View> + <TextField.LabelText> + <Trans>Confirmation code</Trans> + </TextField.LabelText> + <TextField.Root> + <TextField.Input + label={_(msg`Confirmation code`)} + placeholder="XXXXX-XXXXX" + value={resetCode} + onChangeText={setResetCode} + onBlur={onBlur} + autoCapitalize="none" + autoCorrect={false} + autoComplete="one-time-code" + /> + </TextField.Root> + </View> + <View> + <TextField.LabelText> + <Trans>New password</Trans> + </TextField.LabelText> + <TextField.Root> + <TextField.Input + label={_(msg`New password`)} + placeholder={_(msg`At least 8 characters`)} + value={newPassword} + onChangeText={setNewPassword} + secureTextEntry + autoCapitalize="none" + autoComplete="new-password" + /> + </TextField.Root> + </View> + </View> + )} + + <View style={[a.gap_sm]}> + {stage === Stages.RequestCode ? ( + <> + <Button + label={_(msg`Request code`)} + color="primary" + size="large" + disabled={isProcessing} + onPress={onRequestCode}> + <ButtonText> + <Trans>Request code</Trans> + </ButtonText> + {isProcessing && <ButtonIcon icon={Loader} />} + </Button> + <Button + label={_(msg`Already have a code?`)} + onPress={() => setStage(Stages.ChangePassword)} + size="large" + color="primary_subtle" + disabled={isProcessing}> + <ButtonText> + <Trans>Already have a code?</Trans> + </ButtonText> + </Button> + {isNative && ( + <Button + label={_(msg`Cancel`)} + color="secondary" + size="large" + disabled={isProcessing} + onPress={() => control.close()}> + <ButtonText> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + )} + </> + ) : stage === Stages.ChangePassword ? ( + <> + <Button + label={_(msg`Change password`)} + color="primary" + size="large" + disabled={isProcessing} + onPress={onChangePassword}> + <ButtonText> + <Trans>Change password</Trans> + </ButtonText> + {isProcessing && <ButtonIcon icon={Loader} />} + </Button> + <Button + label={_(msg`Back`)} + color="secondary" + size="large" + disabled={isProcessing} + onPress={() => { + setResetCode('') + setStage(Stages.RequestCode) + }}> + <ButtonText> + <Trans>Back</Trans> + </ButtonText> + </Button> + </> + ) : stage === Stages.Done ? ( + <Button + label={_(msg`Close`)} + color="primary" + size="large" + onPress={() => control.close()}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + ) : null} + </View> + </View> + <Dialog.Close /> + </Dialog.ScrollableInner> + ) +} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 9197a66c9..3ebbd1732 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 9b96e7db0..000000000 --- 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>(Stages.RequestCode) - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [resetCode, setResetCode] = useState<string>('') - const [newPassword, setNewPassword] = useState<string>('') - const [error, setError] = useState<string>('') - 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 ( - <SafeAreaView style={[pal.view, s.flex1]}> - <ScrollView - contentContainerStyle={[ - styles.container, - isMobile && styles.containerMobile, - ]} - keyboardShouldPersistTaps="handled"> - <View> - <View style={styles.titleSection}> - <Text type="title-lg" style={[pal.text, styles.title]}> - {stage !== Stages.Done - ? _(msg`Change Password`) - : _(msg`Password Changed`)} - </Text> - </View> - - <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> - {stage === Stages.RequestCode ? ( - <Trans> - If you want to change your password, we will send you a code to - verify that this is your account. - </Trans> - ) : stage === Stages.ChangePassword ? ( - <Trans> - Enter the code you received to change your password. - </Trans> - ) : ( - <Trans>Your password has been changed successfully!</Trans> - )} - </Text> - - {stage === Stages.RequestCode && ( - <View style={[s.flexRow, s.justifyCenter, s.mt10]}> - <TouchableOpacity - testID="skipSendEmailButton" - onPress={() => setStage(Stages.ChangePassword)} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl" style={[pal.link, s.pr5]}> - <Trans>Already have a code?</Trans> - </Text> - </TouchableOpacity> - </View> - )} - {stage === Stages.ChangePassword && ( - <View style={[pal.border, styles.group]}> - <View style={[styles.groupContent]}> - <FontAwesomeIcon - icon="ticket" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="codeInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Reset code`)} - placeholderTextColor={pal.colors.textLight} - value={resetCode} - onChangeText={setResetCode} - onFocus={() => setError('')} - onBlur={onBlur} - accessible={true} - accessibilityLabel={_(msg`Reset Code`)} - accessibilityHint="" - autoCapitalize="none" - autoCorrect={false} - autoComplete="off" - /> - </View> - <View - style={[ - pal.borderDark, - styles.groupContent, - styles.groupBottom, - ]}> - <FontAwesomeIcon - icon="lock" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="codeInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`New password`)} - placeholderTextColor={pal.colors.textLight} - onChangeText={setNewPassword} - secureTextEntry - accessible={true} - accessibilityLabel={_(msg`New Password`)} - accessibilityHint="" - autoCapitalize="none" - autoComplete="new-password" - /> - </View> - </View> - )} - {error ? ( - <ErrorMessage message={error} style={styles.error} /> - ) : undefined} - </View> - <View style={[styles.btnContainer]}> - {isProcessing ? ( - <View style={styles.btn}> - <ActivityIndicator color="#fff" /> - </View> - ) : ( - <View style={{gap: 6}}> - {stage === Stages.RequestCode && ( - <Button - testID="requestChangeBtn" - type="primary" - onPress={onRequestCode} - accessibilityLabel={_(msg`Request Code`)} - accessibilityHint="" - label={_(msg`Request Code`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - {stage === Stages.ChangePassword && ( - <Button - testID="confirmBtn" - type="primary" - onPress={onChangePassword} - accessibilityLabel={_(msg`Next`)} - accessibilityHint="" - label={_(msg`Next`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - <Button - testID="cancelBtn" - type={stage !== Stages.Done ? 'default' : 'primary'} - onPress={() => { - closeModal() - }} - accessibilityLabel={ - stage !== Stages.Done ? _(msg`Cancel`) : _(msg`Close`) - } - accessibilityHint="" - label={stage !== Stages.Done ? _(msg`Cancel`) : _(msg`Close`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - </View> - )} - </View> - </ScrollView> - </SafeAreaView> - ) -} - -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 f9afd183e..c3628f939 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 = <PostLanguagesSettingsModal.Component /> - } else if (activeModal?.name === 'change-password') { - snapPoints = ChangePasswordModal.snapPoints - element = <ChangePasswordModal.Component /> } else { return null } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 3eb744380..08f0e2f85 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 = <ContentLanguagesSettingsModal.Component /> } else if (modal.name === 'post-languages-settings') { element = <PostLanguagesSettingsModal.Component /> - } else if (modal.name === 'change-password') { - element = <ChangePasswordModal.Component /> } else { return null } |