Bluesky app fork with some witchin' additions 馃挮
at readme-update 319 lines 8.9 kB view raw
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}