forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useReducer} from 'react'
2import {View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6import {validate as validateEmail} from 'email-validator'
7
8import {wait} from '#/lib/async/wait'
9import {useCleanError} from '#/lib/hooks/useCleanError'
10import {logger} from '#/logger'
11import {useSession} from '#/state/session'
12import {atoms as a, useTheme} from '#/alf'
13import {Admonition} from '#/components/Admonition'
14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
16import {
17 isValidCode,
18 TokenField,
19} from '#/components/dialogs/EmailDialog/components/TokenField'
20import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
21import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
22import {useUpdateEmail} from '#/components/dialogs/EmailDialog/data/useUpdateEmail'
23import {
24 type ScreenID,
25 type ScreenProps,
26} from '#/components/dialogs/EmailDialog/types'
27import {Divider} from '#/components/Divider'
28import * as TextField from '#/components/forms/TextField'
29import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
30import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
31import {Loader} from '#/components/Loader'
32import {Text} from '#/components/Typography'
33
34type State = {
35 step: 'email' | 'token'
36 mutationStatus: 'pending' | 'success' | 'error' | 'default'
37 error: string
38 emailValid: boolean
39 email: string
40 token: string
41}
42
43type Action =
44 | {
45 type: 'setStep'
46 step: State['step']
47 }
48 | {
49 type: 'setError'
50 error: string
51 }
52 | {
53 type: 'setMutationStatus'
54 status: State['mutationStatus']
55 }
56 | {
57 type: 'setEmail'
58 value: string
59 }
60 | {
61 type: 'setToken'
62 value: string
63 }
64
65function reducer(state: State, action: Action): State {
66 switch (action.type) {
67 case 'setStep': {
68 return {
69 ...state,
70 step: action.step,
71 }
72 }
73 case 'setError': {
74 return {
75 ...state,
76 error: action.error,
77 mutationStatus: 'error',
78 }
79 }
80 case 'setMutationStatus': {
81 return {
82 ...state,
83 error: '',
84 mutationStatus: action.status,
85 }
86 }
87 case 'setEmail': {
88 const emailValid = validateEmail(action.value)
89 return {
90 ...state,
91 step: 'email',
92 token: '',
93 email: action.value,
94 emailValid,
95 }
96 }
97 case 'setToken': {
98 return {
99 ...state,
100 error: '',
101 token: action.value,
102 }
103 }
104 }
105}
106
107export function Update(_props: ScreenProps<ScreenID.Update>) {
108 const t = useTheme()
109 const {_} = useLingui()
110 const cleanError = useCleanError()
111 const {currentAccount} = useSession()
112 const [state, dispatch] = useReducer(reducer, {
113 step: 'email',
114 mutationStatus: 'default',
115 error: '',
116 email: '',
117 emailValid: true,
118 token: '',
119 })
120
121 const {mutateAsync: updateEmail} = useUpdateEmail()
122 const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
123 const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
124
125 const handleEmailChange = (email: string) => {
126 dispatch({
127 type: 'setEmail',
128 value: email,
129 })
130 }
131
132 const handleUpdateEmail = async () => {
133 if (state.step === 'token' && !isValidCode(state.token)) {
134 dispatch({
135 type: 'setError',
136 error: _(msg`Please enter a valid code.`),
137 })
138 return
139 }
140
141 dispatch({
142 type: 'setMutationStatus',
143 status: 'pending',
144 })
145
146 if (state.emailValid === false) {
147 dispatch({
148 type: 'setError',
149 error: _(msg`Please enter a valid email address.`),
150 })
151 return
152 }
153
154 if (state.email === currentAccount!.email) {
155 dispatch({
156 type: 'setError',
157 error: _(msg`This email is already associated with your account.`),
158 })
159 return
160 }
161
162 try {
163 const {status} = await wait(
164 1000,
165 updateEmail({
166 email: state.email,
167 token: state.token,
168 }),
169 )
170
171 if (status === 'tokenRequired') {
172 dispatch({
173 type: 'setStep',
174 step: 'token',
175 })
176 dispatch({
177 type: 'setMutationStatus',
178 status: 'default',
179 })
180 } else if (status === 'success') {
181 dispatch({
182 type: 'setMutationStatus',
183 status: 'success',
184 })
185
186 try {
187 // fire off a confirmation email immediately
188 await requestEmailVerification()
189 } catch {}
190 }
191 } catch (e) {
192 logger.error('EmailDialog: update email failed', {safeMessage: e})
193 const {clean} = cleanError(e)
194 dispatch({
195 type: 'setError',
196 error: clean || _(msg`Failed to update email, please try again.`),
197 })
198 }
199 }
200
201 return (
202 <View style={[a.gap_lg]}>
203 <Text style={[a.text_xl, a.font_bold]}>
204 <Trans>Update your email</Trans>
205 </Text>
206
207 {currentAccount?.emailAuthFactor && (
208 <Admonition type="warning">
209 <Trans>
210 If you update your email address, email 2FA will be disabled.
211 </Trans>
212 </Admonition>
213 )}
214
215 <View style={[a.gap_md]}>
216 <View>
217 <Text style={[a.pb_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
218 <Trans>Please enter your new email address.</Trans>
219 </Text>
220 <TextField.Root>
221 <TextField.Icon icon={Envelope} />
222 <TextField.Input
223 label={_(msg`New email address`)}
224 placeholder={_(msg`alice@example.com`)}
225 defaultValue={state.email}
226 onChangeText={
227 state.mutationStatus === 'success'
228 ? undefined
229 : handleEmailChange
230 }
231 keyboardType="email-address"
232 autoComplete="email"
233 autoCapitalize="none"
234 onSubmitEditing={handleUpdateEmail}
235 />
236 </TextField.Root>
237 </View>
238
239 {state.step === 'token' && (
240 <>
241 <Divider />
242 <View>
243 <Text style={[a.text_md, a.pb_sm, a.font_semi_bold]}>
244 <Trans>Security step required</Trans>
245 </Text>
246 <Text
247 style={[a.pb_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
248 <Trans>
249 Please enter the security code we sent to your previous email
250 address.
251 </Trans>
252 </Text>
253 <TokenField
254 value={state.token}
255 onChangeText={
256 state.mutationStatus === 'success'
257 ? undefined
258 : token => {
259 dispatch({
260 type: 'setToken',
261 value: token,
262 })
263 }
264 }
265 onSubmitEditing={handleUpdateEmail}
266 />
267 {state.mutationStatus !== 'success' && (
268 <ResendEmailText
269 onPress={requestEmailUpdate}
270 style={[a.pt_sm]}
271 />
272 )}
273 </View>
274 </>
275 )}
276
277 {state.error && <Admonition type="error">{state.error}</Admonition>}
278 </View>
279
280 {state.mutationStatus === 'success' ? (
281 <>
282 <Divider />
283 <View style={[a.gap_sm]}>
284 <View style={[a.flex_row, a.gap_sm, a.align_center]}>
285 <Check fill={t.palette.positive_500} size="xs" />
286 <Text style={[a.text_md, a.font_bold]}>
287 <Trans>Success!</Trans>
288 </Text>
289 </View>
290 <Text style={[a.leading_snug]}>
291 <Trans>
292 Please click on the link in the email we just sent you to verify
293 your new email address. This is an important step to allow you
294 to continue enjoying all the features of Bluesky.
295 </Trans>
296 </Text>
297 </View>
298 </>
299 ) : (
300 <Button
301 label={_(msg`Update email`)}
302 size="large"
303 variant="solid"
304 color="primary"
305 onPress={handleUpdateEmail}
306 disabled={
307 !state.email ||
308 (state.step === 'token' &&
309 (!state.token || state.token.length !== 11)) ||
310 state.mutationStatus === 'pending'
311 }>
312 <ButtonText>
313 <Trans>Update email</Trans>
314 </ButtonText>
315 {state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
316 </Button>
317 )}
318 </View>
319 )
320}