forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6
7import {cleanError} from '#/lib/strings/errors'
8import {useAgent, useSession} from '#/state/session'
9import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
10import * as Toast from '#/view/com/util/Toast'
11import {atoms as a, useBreakpoints, useTheme} from '#/alf'
12import {Button, ButtonIcon, ButtonText} from '#/components/Button'
13import * as Dialog from '#/components/Dialog'
14import * as TextField from '#/components/forms/TextField'
15import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
16import {Loader} from '#/components/Loader'
17import {P, Text} from '#/components/Typography'
18import {IS_NATIVE} from '#/env'
19
20enum Stages {
21 Email,
22 ConfirmCode,
23}
24
25export function DisableEmail2FADialog({
26 control,
27}: {
28 control: Dialog.DialogOuterProps['control']
29}) {
30 const {_} = useLingui()
31 const t = useTheme()
32 const {gtMobile} = useBreakpoints()
33 const {currentAccount} = useSession()
34 const agent = useAgent()
35
36 const [stage, setStage] = useState<Stages>(Stages.Email)
37 const [confirmationCode, setConfirmationCode] = useState<string>('')
38 const [isProcessing, setIsProcessing] = useState<boolean>(false)
39 const [error, setError] = useState<string>('')
40
41 const onSendEmail = async () => {
42 setError('')
43 setIsProcessing(true)
44 try {
45 await agent.com.atproto.server.requestEmailUpdate()
46 setStage(Stages.ConfirmCode)
47 } catch (e) {
48 setError(cleanError(String(e)))
49 } finally {
50 setIsProcessing(false)
51 }
52 }
53
54 const onConfirmDisable = async () => {
55 setError('')
56 setIsProcessing(true)
57 try {
58 if (currentAccount?.email) {
59 await agent.com.atproto.server.updateEmail({
60 email: currentAccount.email,
61 token: confirmationCode.trim(),
62 emailAuthFactor: false,
63 })
64 await agent.resumeSession(agent.session!)
65 Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'})))
66 }
67 control.close()
68 } catch (e) {
69 const errMsg = String(e)
70 if (errMsg.includes('Token is invalid')) {
71 setError(_(msg`Invalid 2FA confirmation code.`))
72 } else {
73 setError(cleanError(errMsg))
74 }
75 } finally {
76 setIsProcessing(false)
77 }
78 }
79
80 return (
81 <Dialog.Outer control={control}>
82 <Dialog.Handle />
83 <Dialog.ScrollableInner
84 accessibilityDescribedBy="dialog-description"
85 accessibilityLabelledBy="dialog-title">
86 <View style={[a.relative, a.gap_md, a.w_full]}>
87 <Text
88 nativeID="dialog-title"
89 style={[a.text_2xl, a.font_semi_bold, t.atoms.text]}>
90 <Trans>Disable Email 2FA</Trans>
91 </Text>
92 <P nativeID="dialog-description">
93 {stage === Stages.ConfirmCode ? (
94 <Trans>
95 An email has been sent to{' '}
96 {currentAccount?.email || '(no email)'}. It includes a
97 confirmation code which you can enter below.
98 </Trans>
99 ) : (
100 <Trans>
101 To disable the email 2FA method, please verify your access to
102 the email address.
103 </Trans>
104 )}
105 </P>
106
107 {error ? <ErrorMessage message={error} /> : undefined}
108
109 {stage === Stages.Email ? (
110 <View
111 style={[
112 a.gap_sm,
113 gtMobile && [a.flex_row, a.justify_end, a.gap_md],
114 ]}>
115 <Button
116 testID="sendEmailButton"
117 variant="solid"
118 color="primary"
119 size={gtMobile ? 'small' : 'large'}
120 onPress={onSendEmail}
121 label={_(msg`Send verification email`)}
122 disabled={isProcessing}>
123 <ButtonText>
124 <Trans>Send verification email</Trans>
125 </ButtonText>
126 {isProcessing && <ButtonIcon icon={Loader} />}
127 </Button>
128 <Button
129 testID="haveCodeButton"
130 variant="ghost"
131 color="primary"
132 size={gtMobile ? 'small' : 'large'}
133 onPress={() => setStage(Stages.ConfirmCode)}
134 label={_(msg`I have a code`)}
135 disabled={isProcessing}>
136 <ButtonText>
137 <Trans>I have a code</Trans>
138 </ButtonText>
139 </Button>
140 </View>
141 ) : stage === Stages.ConfirmCode ? (
142 <View>
143 <View style={[a.mb_md]}>
144 <TextField.LabelText>
145 <Trans>Confirmation code</Trans>
146 </TextField.LabelText>
147 <TextField.Root>
148 <TextField.Icon icon={Lock} />
149 <Dialog.Input
150 testID="confirmationCode"
151 label={_(msg`Confirmation code`)}
152 autoCapitalize="none"
153 autoFocus
154 autoCorrect={false}
155 autoComplete="off"
156 value={confirmationCode}
157 onChangeText={setConfirmationCode}
158 onSubmitEditing={onConfirmDisable}
159 editable={!isProcessing}
160 />
161 </TextField.Root>
162 </View>
163 <View
164 style={[
165 a.gap_sm,
166 gtMobile && [a.flex_row, a.justify_end, a.gap_md],
167 ]}>
168 <Button
169 testID="resendCodeBtn"
170 variant="ghost"
171 color="primary"
172 size={gtMobile ? 'small' : 'large'}
173 onPress={onSendEmail}
174 label={_(msg`Resend email`)}
175 disabled={isProcessing}>
176 <ButtonText>
177 <Trans>Resend email</Trans>
178 </ButtonText>
179 </Button>
180 <Button
181 testID="confirmBtn"
182 variant="solid"
183 color="primary"
184 size={gtMobile ? 'small' : 'large'}
185 onPress={onConfirmDisable}
186 label={_(msg`Confirm`)}
187 disabled={isProcessing}>
188 <ButtonText>
189 <Trans>Confirm</Trans>
190 </ButtonText>
191 {isProcessing && <ButtonIcon icon={Loader} />}
192 </Button>
193 </View>
194 </View>
195 ) : undefined}
196
197 {!gtMobile && IS_NATIVE && <View style={{height: 40}} />}
198 </View>
199 </Dialog.ScrollableInner>
200 </Dialog.Outer>
201 )
202}