alf change email flow

This commit is contained in:
Samuel Newman
2025-04-03 14:34:52 +02:00
parent f512979136
commit 5665958dd9
4 changed files with 299 additions and 11 deletions
@@ -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 (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Inner verifyEmailControl={verifyEmailControl} />
</Dialog.Outer>
)
}
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 (
<Dialog.ScrollableInner
label={_(msg`Verify email dialog`)}
style={[
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
]}>
<Dialog.Close />
<View style={[a.gap_xl]}>
<View style={[a.gap_sm]}>
<Text style={[a.font_heavy, a.text_2xl]}>
{uiStrings[currentStep].title}
</Text>
{error ? (
<View style={[a.rounded_sm, a.overflow_hidden]}>
<ErrorMessage message={error} />
</View>
) : null}
{currentStep === 'StepOne' ? (
<View>
<TextField.LabelText>
<Trans>Enter your new email address below.</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Input
label={_(msg`New email address`)}
placeholder="alice@example.com"
defaultValue={email}
onChangeText={setEmail}
keyboardType="email-address"
autoComplete="email"
/>
</TextField.Root>
</View>
) : (
<Text style={[a.text_md, a.leading_snug]}>
{uiStrings[currentStep].message}
</Text>
)}
</View>
{currentStep === 'StepTwo' ? (
<View>
<TextField.LabelText>
<Trans>Confirmation Code</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Input
label={_(msg`Confirmation code`)}
placeholder="XXXXX-XXXXX"
onChangeText={setConfirmationCode}
/>
</TextField.Root>
</View>
) : null}
<View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}>
{currentStep === 'StepOne' ? (
<>
<Button
label={_(msg`Send confirmation email`)}
variant="solid"
color="primary"
size="large"
disabled={isProcessing}
onPress={onRequestChange}>
<ButtonText>
<Trans>Request change</Trans>
</ButtonText>
{isProcessing ? (
<Loader size="sm" style={[{color: 'white'}]} />
) : null}
</Button>
<Button
label={_(msg`I Have a Code`)}
variant="solid"
color="secondary"
size="large"
disabled={isProcessing}
onPress={() => setCurrentStep('StepTwo')}>
<ButtonText>
<Trans>I have a code</Trans>
</ButtonText>
</Button>
</>
) : currentStep === 'StepTwo' ? (
<>
<Button
label={_(msg`Confirm`)}
variant="solid"
color="primary"
size="large"
disabled={isProcessing}
onPress={onConfirm}>
<ButtonText>
<Trans>Confirm</Trans>
</ButtonText>
{isProcessing ? (
<Loader size="sm" style={[{color: 'white'}]} />
) : null}
</Button>
<Button
label={_(msg`Resend Email`)}
variant="solid"
color="secondary"
size="large"
disabled={isProcessing}
onPress={() => {
setConfirmationCode('')
setCurrentStep('StepOne')
}}>
<ButtonText>
<Trans>Resend Email</Trans>
</ButtonText>
</Button>
</>
) : currentStep === 'StepThree' ? (
<>
<Button
label={_(msg`Verify Email`)}
variant="solid"
color="primary"
size="large"
onPress={onVerify}>
<ButtonText>
<Trans>Verify Email</Trans>
</ButtonText>
</Button>
<Button
label={_(msg`Close`)}
variant="solid"
color="secondary"
size="large"
onPress={() => control.close()}>
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
</>
) : null}
</View>
</View>
</Dialog.ScrollableInner>
)
}
+10 -9
View File
@@ -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({
}}>
<Dialog.Handle />
<Inner
control={control}
setDidVerify={setDidVerify}
reasonText={reasonText}
changeEmailControl={changeEmailControl}
/>
</Dialog.Outer>
)
}
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}
</Button>
<Button
label={_(msg`I Have a Code`)}
label={_(msg`I have a code`)}
variant="solid"
color="secondary"
size="large"
disabled={isProcessing}
onPress={() => setCurrentStep('StepTwo')}>
<ButtonText>
<Trans>I Have a Code</Trans>
<Trans>I have a code</Trans>
</ButtonText>
</Button>
</>