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