From 5665958dd9351bcd8da1cfe6298db974c0e9e893 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 3 Apr 2025 14:34:52 +0200 Subject: [PATCH] alf change email flow --- src/components/dialogs/ChangeEmailDialog.tsx | 271 ++++++++++++++++++ src/components/dialogs/VerifyEmailDialog.tsx | 19 +- src/screens/Settings/AccountSettings.tsx | 13 +- .../Settings/components/Email2FAToggle.tsx | 7 + 4 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 src/components/dialogs/ChangeEmailDialog.tsx diff --git a/src/components/dialogs/ChangeEmailDialog.tsx b/src/components/dialogs/ChangeEmailDialog.tsx new file mode 100644 index 0000000000..433e4289b9 --- /dev/null +++ b/src/components/dialogs/ChangeEmailDialog.tsx @@ -0,0 +1,271 @@ +import React, {useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {useAgent, useSession} from '#/state/session' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import {atoms as a, useBreakpoints} from '#/alf' +import {Button, 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' + +export function ChangeEmailDialog({ + control, + verifyEmailControl, +}: { + control: Dialog.DialogControlProps + verifyEmailControl: Dialog.DialogControlProps +}) { + return ( + + + + + ) +} + +export function Inner({ + verifyEmailControl, +}: { + verifyEmailControl: Dialog.DialogControlProps +}) { + const {_} = useLingui() + const {currentAccount} = useSession() + const agent = useAgent() + const control = Dialog.useDialogContext() + const {gtMobile} = useBreakpoints() + + const [currentStep, setCurrentStep] = React.useState< + 'StepOne' | 'StepTwo' | 'StepThree' + >('StepOne') + const [email, setEmail] = useState(currentAccount?.email || '') + const [confirmationCode, setConfirmationCode] = useState('') + const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') + + const currentEmail = currentAccount?.email || '(no email)' + const uiStrings = { + StepOne: { + title: _(msg`Change Your Email`), + message: '', + }, + StepTwo: { + title: _(msg`Security Step Required`), + message: _( + msg` An email has been sent to your previous address, ${currentEmail}. It includes a confirmation code which you can enter below.`, + ), + }, + StepThree: { + title: _(msg`Email Updated!`), + message: _( + msg`Your email has been updated but not verified. As a next step, please verify your new email.`, + ), + }, + } + + const onRequestChange = async () => { + if (email === currentAccount?.email) { + setError( + _( + msg`The email address you entered is the same as your current email address.`, + ), + ) + return + } + setError('') + setIsProcessing(true) + try { + const res = await agent.com.atproto.server.requestEmailUpdate() + if (res.data.tokenRequired) { + setCurrentStep('StepTwo') + } else { + await agent.com.atproto.server.updateEmail({email: email.trim()}) + await agent.resumeSession(agent.session!) + setCurrentStep('StepThree') + } + } catch (e) { + let err = cleanError(String(e)) + // TEMP + // while rollout is occuring, we're giving a temporary error message + // you can remove this any time after Oct2023 + // -prf + if (err === 'email must be confirmed (temporary)') { + err = _( + msg`Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`, + ) + } + setError(err) + } finally { + setIsProcessing(false) + } + } + + const onConfirm = async () => { + setError('') + setIsProcessing(true) + try { + await agent.com.atproto.server.updateEmail({ + email: email.trim(), + token: confirmationCode.trim(), + }) + await agent.resumeSession(agent.session!) + setCurrentStep('StepThree') + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onVerify = async () => { + control.close(() => { + verifyEmailControl.open() + }) + } + + return ( + + + + + + {uiStrings[currentStep].title} + + {error ? ( + + + + ) : null} + {currentStep === 'StepOne' ? ( + + + Enter your new email address below. + + + + + + ) : ( + + {uiStrings[currentStep].message} + + )} + + {currentStep === 'StepTwo' ? ( + + + Confirmation Code + + + + + + ) : null} + + {currentStep === 'StepOne' ? ( + <> + + + + ) : currentStep === 'StepTwo' ? ( + <> + + + + ) : currentStep === 'StepThree' ? ( + <> + + + + ) : null} + + + + ) +} diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx index ced9171ce3..1e01a8e3c5 100644 --- a/src/components/dialogs/VerifyEmailDialog.tsx +++ b/src/components/dialogs/VerifyEmailDialog.tsx @@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' import {useAgent, useSession} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {atoms as a, useBreakpoints} from '#/alf' @@ -21,11 +20,13 @@ export function VerifyEmailDialog({ onCloseWithoutVerifying, onCloseAfterVerifying, reasonText, + changeEmailControl, }: { control: Dialog.DialogControlProps onCloseWithoutVerifying?: () => void onCloseAfterVerifying?: () => void reasonText?: string + changeEmailControl: Dialog.DialogControlProps }) { const agent = useAgent() @@ -50,27 +51,27 @@ export function VerifyEmailDialog({ }}> ) } export function Inner({ - control, setDidVerify, reasonText, + changeEmailControl, }: { - control: Dialog.DialogControlProps setDidVerify: (value: boolean) => void reasonText?: string + changeEmailControl: Dialog.DialogControlProps }) { + const control = Dialog.useDialogContext() const {_} = useLingui() const {currentAccount} = useSession() const agent = useAgent() - const {openModal} = useModalControls() const {gtMobile} = useBreakpoints() const [currentStep, setCurrentStep] = React.useState< @@ -164,7 +165,7 @@ export function Inner({ onPress={e => { e.preventDefault() control.close(() => { - openModal({name: 'change-email'}) + changeEmailControl.open() }) return false }}> @@ -189,7 +190,7 @@ export function Inner({ onPress={e => { e.preventDefault() control.close(() => { - openModal({name: 'change-email'}) + changeEmailControl.open() }) return false }}> @@ -236,14 +237,14 @@ export function Inner({ ) : null} diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index a69c5cdd3d..7c50bd8dff 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -9,6 +9,7 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' @@ -31,6 +32,7 @@ export function AccountSettingsScreen({}: Props) { const {currentAccount} = useSession() const {openModal} = useModalControls() const verifyEmailControl = useDialogControl() + const changeEmailControl = useDialogControl() const birthdayControl = useDialogControl() const changeHandleControl = useDialogControl() const exportCarControl = useDialogControl() @@ -95,7 +97,7 @@ export function AccountSettingsScreen({}: Props) { )} openModal({name: 'change-email'})}> + onPress={() => changeEmailControl.open()}> Change email @@ -165,7 +167,14 @@ export function AccountSettingsScreen({}: Props) { - + + diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx index 434792fb48..810e843d24 100644 --- a/src/screens/Settings/components/Email2FAToggle.tsx +++ b/src/screens/Settings/components/Email2FAToggle.tsx @@ -4,6 +4,7 @@ import {useLingui} from '@lingui/react' import {useAgent, useSession} from '#/state/session' import {useDialogControl} from '#/components/Dialog' +import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import * as Prompt from '#/components/Prompt' import {DisableEmail2FADialog} from './DisableEmail2FADialog' @@ -15,6 +16,7 @@ export function Email2FAToggle() { const disableDialogControl = useDialogControl() const enableDialogControl = useDialogControl() const verifyEmailDialogControl = useDialogControl() + const changeEmailDialogControl = useDialogControl() const agent = useAgent() const enableEmailAuthFactor = React.useCallback(async () => { @@ -59,11 +61,16 @@ export function Email2FAToggle() { /> +