Files
bsky-social-app/src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx
T
2026-05-29 11:03:00 -07:00

253 lines
7.2 KiB
TypeScript

import {useReducer, useState} from 'react'
import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
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 {useDialogContext} from '#/components/Dialog'
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
import {
isValidCode,
TokenField,
} from '#/components/dialogs/EmailDialog/components/TokenField'
import {useManageEmail2FA} from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
import {Divider} from '#/components/Divider'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {createStaticClick, InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Span, Text} from '#/components/Typography'
type State = {
error: string
step: 'email' | 'token'
emailStatus: 'pending' | 'success' | 'error' | 'default'
tokenStatus: 'pending' | 'success' | 'error' | 'default'
}
type Action =
| {
type: 'setError'
error: string
}
| {
type: 'setStep'
step: 'email' | 'token'
}
| {
type: 'setEmailStatus'
status: State['emailStatus']
}
| {
type: 'setTokenStatus'
status: State['tokenStatus']
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'setError': {
return {
...state,
error: action.error,
emailStatus: 'error',
tokenStatus: 'error',
}
}
case 'setStep': {
return {
...state,
error: '',
step: action.step,
}
}
case 'setEmailStatus': {
return {
...state,
error: '',
emailStatus: action.status,
}
}
case 'setTokenStatus': {
return {
...state,
error: '',
tokenStatus: action.status,
}
}
default: {
return state
}
}
}
export function Disable() {
const t = useTheme()
const {t: l} = useLingui()
const cleanError = useCleanError()
const {currentAccount} = useSession()
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
const {mutateAsync: manageEmail2FA} = useManageEmail2FA()
const control = useDialogContext()
const [token, setToken] = useState('')
const [state, dispatch] = useReducer(reducer, {
error: '',
step: 'email',
emailStatus: 'default',
tokenStatus: 'default',
})
const handleSendEmail = async () => {
dispatch({type: 'setEmailStatus', status: 'pending'})
try {
await wait(1000, requestEmailUpdate())
dispatch({type: 'setEmailStatus', status: 'success'})
setTimeout(() => {
dispatch({type: 'setStep', step: 'token'})
}, 1000)
} catch (e) {
logger.error('Manage2FA: email update code request failed', {
safeMessage: e,
})
const {clean} = cleanError(e)
dispatch({
type: 'setError',
error: clean || l`Failed to send email, please try again.`,
})
}
}
const handleManageEmail2FA = async () => {
if (!isValidCode(token)) {
dispatch({
type: 'setError',
error: l`Please enter a valid code.`,
})
return
}
dispatch({type: 'setTokenStatus', status: 'pending'})
try {
await wait(1000, manageEmail2FA({enabled: false, token}))
dispatch({type: 'setTokenStatus', status: 'success'})
setTimeout(() => {
control.close()
}, 1000)
} catch (e) {
logger.error('Manage2FA: disable email 2FA failed', {safeMessage: e})
const {clean} = cleanError(e)
dispatch({
type: 'setError',
error: clean || l`Failed to update email 2FA settings`,
})
}
}
return (
<View style={[a.gap_sm]}>
<Text style={[a.text_xl, a.font_bold, a.leading_snug]}>
<Trans>Disable email 2FA</Trans>
</Text>
{state.step === 'email' ? (
<>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
To disable your email 2FA method, please verify your access to{' '}
<Span style={[a.font_semi_bold]}>{currentAccount?.email}</Span>
</Trans>
</Text>
<View style={[a.gap_lg, a.pt_sm]}>
{state.error && <Admonition type="error">{state.error}</Admonition>}
<Button
label={l`Send email`}
size="large"
variant="solid"
color="primary"
onPress={handleSendEmail}
disabled={state.emailStatus === 'pending'}>
<ButtonText>
<Trans>Send email</Trans>
</ButtonText>
<ButtonIcon
icon={
state.emailStatus === 'pending'
? Loader
: state.emailStatus === 'success'
? Check
: Envelope
}
/>
</Button>
<Divider />
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
Have a code?{' '}
<InlineLinkText
label={l`Enter code`}
{...createStaticClick(() => {
dispatch({type: 'setStep', step: 'token'})
})}>
Click here.
</InlineLinkText>
</Trans>
</Text>
</View>
</>
) : (
<>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
To disable your email 2FA method, please verify your access to{' '}
<Span style={[a.font_semi_bold]}>{currentAccount?.email}</Span>
</Trans>
</Text>
<View style={[a.gap_sm, a.py_sm]}>
<TokenField
value={token}
onChangeText={setToken}
onSubmitEditing={handleManageEmail2FA}
/>
<ResendEmailText onPress={handleSendEmail} />
</View>
{state.error && <Admonition type="error">{state.error}</Admonition>}
<Button
label={l`Disable 2FA`}
size="large"
variant="solid"
color="primary"
onPress={handleManageEmail2FA}
disabled={
!token || token.length !== 11 || state.tokenStatus === 'pending'
}>
<ButtonText>
<Trans>Disable 2FA</Trans>
</ButtonText>
{state.tokenStatus === 'pending' ? (
<ButtonIcon icon={Loader} />
) : state.tokenStatus === 'success' ? (
<ButtonIcon icon={Check} />
) : null}
</Button>
</>
)}
</View>
)
}