Files
bsky-social-app/src/components/dialogs/EmailDialog/screens/Update.tsx
T
Eric Bailey 242774abc8 Update copy
2025-05-07 09:24:32 -05:00

306 lines
8.3 KiB
TypeScript

import {useReducer} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {validate as validateEmail} from 'email-validator'
import {wait} from '#/lib/async/wait'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {logger} from '#/logger'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
import {useUpdateEmail} from '#/components/dialogs/EmailDialog/data/useUpdateEmail'
import {
type ScreenID,
type ScreenProps,
} from '#/components/dialogs/EmailDialog/types'
import {Divider} from '#/components/Divider'
import * as TextField from '#/components/forms/TextField'
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
type State = {
step: 'email' | 'token'
mutationStatus: 'pending' | 'success' | 'error' | 'default'
error: string
emailValid: boolean
email: string
token: string
}
type Action =
| {
type: 'setStep'
step: State['step']
}
| {
type: 'setError'
error: string
}
| {
type: 'setMutationStatus'
status: State['mutationStatus']
}
| {
type: 'setEmail'
value: string
}
| {
type: 'setToken'
value: string
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'setStep': {
return {
...state,
step: action.step,
}
}
case 'setError': {
return {
...state,
error: action.error,
mutationStatus: 'error',
}
}
case 'setMutationStatus': {
return {
...state,
error: '',
mutationStatus: action.status,
}
}
case 'setEmail': {
const emailValid = validateEmail(action.value)
return {
...state,
step: 'email',
token: '',
email: action.value,
emailValid,
}
}
case 'setToken': {
return {
...state,
error: '',
token: action.value,
}
}
}
}
export function Update(_props: ScreenProps<ScreenID.Update>) {
const t = useTheme()
const {_} = useLingui()
const cleanError = useCleanError()
const {currentAccount} = useSession()
const [state, dispatch] = useReducer(reducer, {
step: 'email',
mutationStatus: 'default',
error: '',
email: '',
emailValid: true,
token: '',
})
const {mutateAsync: updateEmail} = useUpdateEmail()
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
const handleEmailChange = (email: string) => {
dispatch({
type: 'setEmail',
value: email,
})
}
const handleUpdateEmail = async () => {
dispatch({
type: 'setMutationStatus',
status: 'pending',
})
if (state.emailValid === false) {
dispatch({
type: 'setError',
error: _(msg`Please enter a valid email address.`),
})
return
}
if (state.email === currentAccount!.email) {
dispatch({
type: 'setError',
error: _(msg`This email is already associated with your account.`),
})
return
}
try {
const {status} = await wait(
1000,
updateEmail({
email: state.email,
token: state.token,
}),
)
if (status === 'tokenRequired') {
dispatch({
type: 'setStep',
step: 'token',
})
dispatch({
type: 'setMutationStatus',
status: 'default',
})
} else if (status === 'success') {
dispatch({
type: 'setMutationStatus',
status: 'success',
})
try {
// fire off a confirmation email immediately
await requestEmailVerification()
} catch {}
}
} catch (e) {
logger.error('EmailDialog: update email failed', {safeMessage: e})
const {clean} = cleanError(e)
dispatch({
type: 'setError',
error: clean || _(msg`Failed to update email, please try again.`),
})
}
}
return (
<View style={[a.gap_lg]}>
<Text style={[a.text_xl, a.font_heavy]}>
<Trans>Update your email</Trans>
</Text>
{currentAccount?.emailAuthFactor && (
<Admonition type="warning">
<Trans>
If you update your email address, email 2FA will be disabled.
</Trans>
</Admonition>
)}
<View style={[a.gap_md]}>
<View>
<Text style={[a.pb_sm, t.atoms.text_contrast_medium]}>
<Trans>Please enter your new email address.</Trans>
</Text>
<TextField.Root>
<TextField.Icon icon={Envelope} />
<TextField.Input
label={_(msg`New email address`)}
placeholder={_(msg`alice@example.com`)}
defaultValue={state.email}
onChangeText={
state.mutationStatus === 'success'
? undefined
: handleEmailChange
}
keyboardType="email-address"
autoComplete="email"
autoCapitalize="none"
onSubmitEditing={handleUpdateEmail}
/>
</TextField.Root>
</View>
{state.step === 'token' && (
<>
<Divider />
<View>
<Text style={[a.text_md, a.pb_sm, a.font_bold]}>
<Trans>Security step required</Trans>
</Text>
<Text style={[a.pb_sm, t.atoms.text_contrast_medium]}>
<Trans>
Please enter the security code we sent to your previous email
address.
</Trans>
</Text>
<TokenField
value={state.token}
onChangeText={
state.mutationStatus === 'success'
? undefined
: token => {
dispatch({
type: 'setToken',
value: token,
})
}
}
onSubmitEditing={handleUpdateEmail}
/>
{state.mutationStatus !== 'success' && (
<ResendEmailText
onPress={requestEmailUpdate}
style={[a.pt_sm]}
/>
)}
</View>
</>
)}
{state.error && <Admonition type="error">{state.error}</Admonition>}
</View>
{state.mutationStatus === 'success' ? (
<>
<Divider />
<View style={[a.gap_sm]}>
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
<Check fill={t.palette.positive_600} size="xs" />
<Text style={[a.text_md, a.font_heavy]}>
<Trans>Success!</Trans>
</Text>
</View>
<Text style={[a.leading_snug]}>
<Trans>
Please click on the link in the email we just sent you to verify
your email.
</Trans>
</Text>
</View>
</>
) : (
<Button
label={_(msg`Update email`)}
size="large"
variant="solid"
color="primary"
onPress={handleUpdateEmail}
disabled={
!state.email ||
(state.step === 'token' && !state.token) ||
state.mutationStatus === 'pending'
}>
<ButtonText>
<Trans>Update email</Trans>
</ButtonText>
{state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
</Button>
)}
</View>
)
}