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