Bluesky app fork with some witchin' additions 💫

Modernise change email flow (#8106)

* use new verify email dialog in 2fa flow

* alf change email flow

* Fallback change email dialog

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update ChangeEmailDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update Email2FAToggle.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* don't use existing email as default value

* increase max width of email dialogs

* Use ALF verify email dialog for reminder (#5924)

* use new verify email dialog for reminder

* style tweaks, improve web

* add a lil toast

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Ditch close and push up image

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>

* delete old change/verify email modals (#8122)

(cherry picked from commit fceb655b3bacad1bce210810234137b7233d263d)

* Translate email placeholder

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Align copy

* Clean up error handling

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>

authored by samuel.fm

surfdude29
Eric Bailey
and committed by
GitHub
70dbc947 118d385b

+462 -728
+38 -34
src/Navigation.tsx
··· 33 33 import {bskyTitle} from '#/lib/strings/headings' 34 34 import {logger} from '#/logger' 35 35 import {isNative, isWeb} from '#/platform/detection' 36 - import {useModalControls} from '#/state/modals' 37 36 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 38 37 import {useSession} from '#/state/session' 39 38 import { ··· 80 79 import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers' 81 80 import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows' 82 81 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' 82 + import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch' 83 83 import {SearchScreen} from '#/screens/Search' 84 + import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings' 85 + import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' 86 + import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings' 84 87 import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' 85 88 import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' 89 + import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' 90 + import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' 91 + import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' 92 + import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' 93 + import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' 86 94 import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' 95 + import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' 96 + import {SettingsScreen} from '#/screens/Settings/Settings' 87 97 import {SettingsInterests} from '#/screens/Settings/SettingsInterests' 98 + import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' 88 99 import { 89 100 StarterPackScreen, 90 101 StarterPackScreenShort, 91 102 } from '#/screens/StarterPack/StarterPackScreen' 92 103 import {Wizard} from '#/screens/StarterPack/Wizard' 104 + import TopicScreen from '#/screens/Topic' 93 105 import {VideoFeed} from '#/screens/VideoFeed' 94 106 import {useTheme} from '#/alf' 107 + import {useDialogControl} from '#/components/Dialog' 108 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 95 109 import {router} from '#/routes' 96 110 import {Referrer} from '../modules/expo-bluesky-swiss-army' 97 - import {ProfileSearchScreen} from './screens/Profile/ProfileSearch' 98 - import {AboutSettingsScreen} from './screens/Settings/AboutSettings' 99 - import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' 100 - import {AccountSettingsScreen} from './screens/Settings/AccountSettings' 101 - import {AppPasswordsScreen} from './screens/Settings/AppPasswords' 102 - import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' 103 - import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' 104 - import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' 105 - import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' 106 - import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' 107 - import {SettingsScreen} from './screens/Settings/Settings' 108 - import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' 109 - import TopicScreen from './screens/Topic' 110 111 111 112 const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 112 113 ··· 736 737 function RoutesContainer({children}: React.PropsWithChildren<{}>) { 737 738 const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) 738 739 const {currentAccount} = useSession() 739 - const {openModal} = useModalControls() 740 740 const prevLoggedRouteName = React.useRef<string | undefined>(undefined) 741 + const verifyEmailDialogControl = useDialogControl() 741 742 742 743 function onReady() { 743 744 prevLoggedRouteName.current = getCurrentRouteName() 744 745 if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { 745 - openModal({name: 'verify-email', showReminder: true}) 746 + verifyEmailDialogControl.open() 746 747 snoozeEmailConfirmationPrompt() 747 748 } 748 749 } 749 750 750 751 return ( 751 - <NavigationContainer 752 - ref={navigationRef} 753 - linking={LINKING} 754 - theme={theme} 755 - onStateChange={() => { 756 - logger.metric('router:navigate', { 757 - from: prevLoggedRouteName.current, 758 - }) 759 - prevLoggedRouteName.current = getCurrentRouteName() 760 - }} 761 - onReady={() => { 762 - attachRouteToLogEvents(getCurrentRouteName) 763 - logModuleInitTime() 764 - onReady() 765 - logger.metric('router:navigate', {}) 766 - }}> 767 - {children} 768 - </NavigationContainer> 752 + <> 753 + <NavigationContainer 754 + ref={navigationRef} 755 + linking={LINKING} 756 + theme={theme} 757 + onStateChange={() => { 758 + logger.metric('router:navigate', { 759 + from: prevLoggedRouteName.current, 760 + }) 761 + prevLoggedRouteName.current = getCurrentRouteName() 762 + }} 763 + onReady={() => { 764 + attachRouteToLogEvents(getCurrentRouteName) 765 + logModuleInitTime() 766 + onReady() 767 + logger.metric('router:navigate', {}) 768 + }}> 769 + {children} 770 + </NavigationContainer> 771 + <VerifyEmailDialog control={verifyEmailDialogControl} reminder /> 772 + </> 769 773 ) 770 774 } 771 775
+259
src/components/dialogs/ChangeEmailDialog.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {cleanError} from '#/lib/strings/errors' 7 + import {useAgent, useSession} from '#/state/session' 8 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 9 + import {atoms as a, useBreakpoints, web} from '#/alf' 10 + import {Button, ButtonText} from '#/components/Button' 11 + import * as Dialog from '#/components/Dialog' 12 + import * as TextField from '#/components/forms/TextField' 13 + import {Loader} from '#/components/Loader' 14 + import {Text} from '#/components/Typography' 15 + 16 + export function ChangeEmailDialog({ 17 + control, 18 + verifyEmailControl, 19 + }: { 20 + control: Dialog.DialogControlProps 21 + verifyEmailControl: Dialog.DialogControlProps 22 + }) { 23 + return ( 24 + <Dialog.Outer control={control}> 25 + <Dialog.Handle /> 26 + <Inner verifyEmailControl={verifyEmailControl} /> 27 + </Dialog.Outer> 28 + ) 29 + } 30 + 31 + export function Inner({ 32 + verifyEmailControl, 33 + }: { 34 + verifyEmailControl: Dialog.DialogControlProps 35 + }) { 36 + const {_} = useLingui() 37 + const {currentAccount} = useSession() 38 + const agent = useAgent() 39 + const control = Dialog.useDialogContext() 40 + const {gtMobile} = useBreakpoints() 41 + 42 + const [currentStep, setCurrentStep] = useState< 43 + 'StepOne' | 'StepTwo' | 'StepThree' 44 + >('StepOne') 45 + const [email, setEmail] = useState('') 46 + const [confirmationCode, setConfirmationCode] = useState('') 47 + const [isProcessing, setIsProcessing] = useState(false) 48 + const [error, setError] = useState('') 49 + 50 + const currentEmail = currentAccount?.email || '(no email)' 51 + const uiStrings = { 52 + StepOne: { 53 + title: _(msg`Change Your Email`), 54 + message: '', 55 + }, 56 + StepTwo: { 57 + title: _(msg`Security Step Required`), 58 + message: _( 59 + msg`An email has been sent to your previous address, ${currentEmail}. It includes a confirmation code which you can enter below.`, 60 + ), 61 + }, 62 + StepThree: { 63 + title: _(msg`Email Updated!`), 64 + message: _( 65 + msg`Your email address has been updated but it is not yet verified. As a next step, please verify your new email.`, 66 + ), 67 + }, 68 + } 69 + 70 + const onRequestChange = async () => { 71 + if (email === currentAccount?.email) { 72 + setError( 73 + _( 74 + msg`The email address you entered is the same as your current email address.`, 75 + ), 76 + ) 77 + return 78 + } 79 + setError('') 80 + setIsProcessing(true) 81 + try { 82 + const res = await agent.com.atproto.server.requestEmailUpdate() 83 + if (res.data.tokenRequired) { 84 + setCurrentStep('StepTwo') 85 + } else { 86 + await agent.com.atproto.server.updateEmail({email: email.trim()}) 87 + await agent.resumeSession(agent.session!) 88 + setCurrentStep('StepThree') 89 + } 90 + } catch (e) { 91 + setError(cleanError(String(e))) 92 + } finally { 93 + setIsProcessing(false) 94 + } 95 + } 96 + 97 + const onConfirm = async () => { 98 + setError('') 99 + setIsProcessing(true) 100 + try { 101 + await agent.com.atproto.server.updateEmail({ 102 + email: email.trim(), 103 + token: confirmationCode.trim(), 104 + }) 105 + await agent.resumeSession(agent.session!) 106 + setCurrentStep('StepThree') 107 + } catch (e) { 108 + setError(cleanError(String(e))) 109 + } finally { 110 + setIsProcessing(false) 111 + } 112 + } 113 + 114 + const onVerify = async () => { 115 + control.close(() => { 116 + verifyEmailControl.open() 117 + }) 118 + } 119 + 120 + return ( 121 + <Dialog.ScrollableInner 122 + label={_(msg`Verify email dialog`)} 123 + style={web({maxWidth: 450})}> 124 + <Dialog.Close /> 125 + <View style={[a.gap_xl]}> 126 + <View style={[a.gap_sm]}> 127 + <Text style={[a.font_heavy, a.text_2xl]}> 128 + {uiStrings[currentStep].title} 129 + </Text> 130 + {error ? ( 131 + <View style={[a.rounded_sm, a.overflow_hidden]}> 132 + <ErrorMessage message={error} /> 133 + </View> 134 + ) : null} 135 + {currentStep === 'StepOne' ? ( 136 + <View> 137 + <TextField.LabelText> 138 + <Trans>Enter your new email address below.</Trans> 139 + </TextField.LabelText> 140 + <TextField.Root> 141 + <TextField.Input 142 + label={_(msg`New email address`)} 143 + placeholder={_(msg`alice@example.com`)} 144 + defaultValue={email} 145 + onChangeText={setEmail} 146 + keyboardType="email-address" 147 + autoComplete="email" 148 + /> 149 + </TextField.Root> 150 + </View> 151 + ) : ( 152 + <Text style={[a.text_md, a.leading_snug]}> 153 + {uiStrings[currentStep].message} 154 + </Text> 155 + )} 156 + </View> 157 + {currentStep === 'StepTwo' ? ( 158 + <View> 159 + <TextField.LabelText> 160 + <Trans>Confirmation code</Trans> 161 + </TextField.LabelText> 162 + <TextField.Root> 163 + <TextField.Input 164 + label={_(msg`Confirmation code`)} 165 + placeholder="XXXXX-XXXXX" 166 + onChangeText={setConfirmationCode} 167 + /> 168 + </TextField.Root> 169 + </View> 170 + ) : null} 171 + <View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}> 172 + {currentStep === 'StepOne' ? ( 173 + <> 174 + <Button 175 + label={_(msg`Request change`)} 176 + variant="solid" 177 + color="primary" 178 + size="large" 179 + disabled={isProcessing} 180 + onPress={onRequestChange}> 181 + <ButtonText> 182 + <Trans>Request change</Trans> 183 + </ButtonText> 184 + {isProcessing ? ( 185 + <Loader size="sm" style={[{color: 'white'}]} /> 186 + ) : null} 187 + </Button> 188 + <Button 189 + label={_(msg`I have a code`)} 190 + variant="solid" 191 + color="secondary" 192 + size="large" 193 + disabled={isProcessing} 194 + onPress={() => setCurrentStep('StepTwo')}> 195 + <ButtonText> 196 + <Trans>I have a code</Trans> 197 + </ButtonText> 198 + </Button> 199 + </> 200 + ) : currentStep === 'StepTwo' ? ( 201 + <> 202 + <Button 203 + label={_(msg`Confirm`)} 204 + variant="solid" 205 + color="primary" 206 + size="large" 207 + disabled={isProcessing} 208 + onPress={onConfirm}> 209 + <ButtonText> 210 + <Trans>Confirm</Trans> 211 + </ButtonText> 212 + {isProcessing ? ( 213 + <Loader size="sm" style={[{color: 'white'}]} /> 214 + ) : null} 215 + </Button> 216 + <Button 217 + label={_(msg`Resend email`)} 218 + variant="solid" 219 + color="secondary" 220 + size="large" 221 + disabled={isProcessing} 222 + onPress={() => { 223 + setConfirmationCode('') 224 + setCurrentStep('StepOne') 225 + }}> 226 + <ButtonText> 227 + <Trans>Resend email</Trans> 228 + </ButtonText> 229 + </Button> 230 + </> 231 + ) : currentStep === 'StepThree' ? ( 232 + <> 233 + <Button 234 + label={_(msg`Verify email`)} 235 + variant="solid" 236 + color="primary" 237 + size="large" 238 + onPress={onVerify}> 239 + <ButtonText> 240 + <Trans>Verify email</Trans> 241 + </ButtonText> 242 + </Button> 243 + <Button 244 + label={_(msg`Close`)} 245 + variant="solid" 246 + color="secondary" 247 + size="large" 248 + onPress={() => control.close()}> 249 + <ButtonText> 250 + <Trans>Close</Trans> 251 + </ButtonText> 252 + </Button> 253 + </> 254 + ) : null} 255 + </View> 256 + </View> 257 + </Dialog.ScrollableInner> 258 + ) 259 + }
+111 -48
src/components/dialogs/VerifyEmailDialog.tsx
··· 1 - import React from 'react' 1 + import {useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {cleanError} from '#/lib/strings/errors' 7 7 import {logger} from '#/logger' 8 - import {useModalControls} from '#/state/modals' 9 8 import {useAgent, useSession} from '#/state/session' 10 9 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 11 - import {atoms as a, useBreakpoints} from '#/alf' 10 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 12 11 import {Button, ButtonText} from '#/components/Button' 13 12 import * as Dialog from '#/components/Dialog' 14 13 import * as TextField from '#/components/forms/TextField' 14 + import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' 15 15 import {InlineLinkText} from '#/components/Link' 16 16 import {Loader} from '#/components/Loader' 17 17 import {Text} from '#/components/Typography' 18 + import {ChangeEmailDialog} from './ChangeEmailDialog' 18 19 19 20 export function VerifyEmailDialog({ 20 21 control, 21 22 onCloseWithoutVerifying, 22 23 onCloseAfterVerifying, 23 24 reasonText, 25 + changeEmailControl, 26 + reminder, 24 27 }: { 25 28 control: Dialog.DialogControlProps 26 29 onCloseWithoutVerifying?: () => void 27 30 onCloseAfterVerifying?: () => void 28 31 reasonText?: string 32 + /** 33 + * if a changeEmailControl for a ChangeEmailDialog is not provided, 34 + * this component will create one for you. Using this prop 35 + * helps reduce duplication, since these dialogs are often used together. 36 + */ 37 + changeEmailControl?: Dialog.DialogControlProps 38 + reminder?: boolean 29 39 }) { 30 40 const agent = useAgent() 41 + const fallbackChangeEmailControl = Dialog.useDialogControl() 31 42 32 - const [didVerify, setDidVerify] = React.useState(false) 43 + const [didVerify, setDidVerify] = useState(false) 33 44 34 45 return ( 35 - <Dialog.Outer 36 - control={control} 37 - onClose={async () => { 38 - if (!didVerify) { 39 - onCloseWithoutVerifying?.() 40 - return 41 - } 42 - 43 - try { 44 - await agent.resumeSession(agent.session!) 45 - onCloseAfterVerifying?.() 46 - } catch (e: unknown) { 47 - logger.error(String(e)) 48 - return 49 - } 50 - }}> 51 - <Dialog.Handle /> 52 - <Inner 46 + <> 47 + <Dialog.Outer 53 48 control={control} 54 - setDidVerify={setDidVerify} 55 - reasonText={reasonText} 56 - /> 57 - </Dialog.Outer> 49 + onClose={async () => { 50 + if (!didVerify) { 51 + onCloseWithoutVerifying?.() 52 + return 53 + } 54 + 55 + try { 56 + await agent.resumeSession(agent.session!) 57 + onCloseAfterVerifying?.() 58 + } catch (e: unknown) { 59 + logger.error(String(e)) 60 + return 61 + } 62 + }}> 63 + <Dialog.Handle /> 64 + <Inner 65 + setDidVerify={setDidVerify} 66 + reasonText={reasonText} 67 + changeEmailControl={changeEmailControl ?? fallbackChangeEmailControl} 68 + reminder={reminder} 69 + /> 70 + </Dialog.Outer> 71 + {!changeEmailControl && ( 72 + <ChangeEmailDialog 73 + control={fallbackChangeEmailControl} 74 + verifyEmailControl={control} 75 + /> 76 + )} 77 + </> 58 78 ) 59 79 } 60 80 61 81 export function Inner({ 62 - control, 63 82 setDidVerify, 64 83 reasonText, 84 + changeEmailControl, 85 + reminder, 65 86 }: { 66 - control: Dialog.DialogControlProps 67 87 setDidVerify: (value: boolean) => void 68 88 reasonText?: string 89 + changeEmailControl: Dialog.DialogControlProps 90 + reminder?: boolean 69 91 }) { 92 + const control = Dialog.useDialogContext() 70 93 const {_} = useLingui() 71 94 const {currentAccount} = useSession() 72 95 const agent = useAgent() 73 - const {openModal} = useModalControls() 74 96 const {gtMobile} = useBreakpoints() 97 + const t = useTheme() 75 98 76 - const [currentStep, setCurrentStep] = React.useState< 77 - 'StepOne' | 'StepTwo' | 'StepThree' 78 - >('StepOne') 79 - const [confirmationCode, setConfirmationCode] = React.useState('') 80 - const [isProcessing, setIsProcessing] = React.useState(false) 81 - const [error, setError] = React.useState('') 99 + const [currentStep, setCurrentStep] = useState< 100 + 'Reminder' | 'StepOne' | 'StepTwo' | 'StepThree' 101 + >(reminder ? 'Reminder' : 'StepOne') 102 + const [confirmationCode, setConfirmationCode] = useState('') 103 + const [isProcessing, setIsProcessing] = useState(false) 104 + const [error, setError] = useState('') 82 105 83 106 const uiStrings = { 107 + Reminder: { 108 + title: _(msg`Please Verify Your Email`), 109 + message: _( 110 + msg`Your email has not yet been verified. This is an important security step which we recommend.`, 111 + ), 112 + }, 84 113 StepOne: { 85 114 title: _(msg`Verify Your Email`), 86 115 message: '', ··· 132 161 return ( 133 162 <Dialog.ScrollableInner 134 163 label={_(msg`Verify email dialog`)} 135 - style={[ 136 - gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, 137 - ]}> 138 - <Dialog.Close /> 164 + style={web({maxWidth: 450})}> 139 165 <View style={[a.gap_xl]}> 166 + {currentStep === 'Reminder' && ( 167 + <View 168 + style={[ 169 + a.rounded_sm, 170 + a.align_center, 171 + a.justify_center, 172 + {height: 150}, 173 + t.atoms.bg_contrast_100, 174 + ]}> 175 + <EnvelopeIcon width={64} fill="white" /> 176 + </View> 177 + )} 140 178 <View style={[a.gap_sm]}> 141 179 <Text style={[a.font_heavy, a.text_2xl]}> 142 180 {uiStrings[currentStep].title} ··· 164 202 onPress={e => { 165 203 e.preventDefault() 166 204 control.close(() => { 167 - openModal({name: 'change-email'}) 205 + changeEmailControl.open() 168 206 }) 169 207 return false 170 208 }}> ··· 189 227 onPress={e => { 190 228 e.preventDefault() 191 229 control.close(() => { 192 - openModal({name: 'change-email'}) 230 + changeEmailControl.open() 193 231 }) 194 232 return false 195 233 }}> ··· 219 257 </View> 220 258 ) : null} 221 259 <View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}> 222 - {currentStep === 'StepOne' ? ( 260 + {currentStep === 'Reminder' ? ( 261 + <> 262 + <Button 263 + label={_(msg`Get started`)} 264 + variant="solid" 265 + color="primary" 266 + size="large" 267 + onPress={() => setCurrentStep('StepOne')}> 268 + <ButtonText> 269 + <Trans>Get started</Trans> 270 + </ButtonText> 271 + </Button> 272 + <Button 273 + label={_(msg`Maybe later`)} 274 + accessibilityHint={_(msg`Snoozes the reminder`)} 275 + variant="ghost" 276 + color="secondary" 277 + size="large" 278 + disabled={isProcessing} 279 + onPress={() => control.close()}> 280 + <ButtonText> 281 + <Trans>Maybe later</Trans> 282 + </ButtonText> 283 + </Button> 284 + </> 285 + ) : currentStep === 'StepOne' ? ( 223 286 <> 224 287 <Button 225 288 label={_(msg`Send confirmation email`)} ··· 229 292 disabled={isProcessing} 230 293 onPress={onSendEmail}> 231 294 <ButtonText> 232 - <Trans>Send Confirmation</Trans> 295 + <Trans>Send confirmation</Trans> 233 296 </ButtonText> 234 297 {isProcessing ? ( 235 298 <Loader size="sm" style={[{color: 'white'}]} /> 236 299 ) : null} 237 300 </Button> 238 301 <Button 239 - label={_(msg`I Have a Code`)} 302 + label={_(msg`I have a code`)} 240 303 variant="solid" 241 304 color="secondary" 242 305 size="large" 243 306 disabled={isProcessing} 244 307 onPress={() => setCurrentStep('StepTwo')}> 245 308 <ButtonText> 246 - <Trans>I Have a Code</Trans> 309 + <Trans>I have a code</Trans> 247 310 </ButtonText> 248 311 </Button> 249 312 </> ··· 264 327 ) : null} 265 328 </Button> 266 329 <Button 267 - label={_(msg`Resend Email`)} 330 + label={_(msg`Resend email`)} 268 331 variant="solid" 269 332 color="secondary" 270 333 size="large" ··· 274 337 setCurrentStep('StepOne') 275 338 }}> 276 339 <ButtonText> 277 - <Trans>Resend Email</Trans> 340 + <Trans>Resend email</Trans> 278 341 </ButtonText> 279 342 </Button> 280 343 </> 281 344 ) : currentStep === 'StepThree' ? ( 282 345 <Button 283 - label={_(msg`Confirm`)} 346 + label={_(msg`Close`)} 284 347 variant="solid" 285 348 color="primary" 286 349 size="large"
+11 -2
src/screens/Settings/AccountSettings.tsx
··· 9 9 import {atoms as a, useTheme} from '#/alf' 10 10 import {useDialogControl} from '#/components/Dialog' 11 11 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 12 + import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog' 12 13 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 13 14 import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' 14 15 import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' ··· 31 32 const {currentAccount} = useSession() 32 33 const {openModal} = useModalControls() 33 34 const verifyEmailControl = useDialogControl() 35 + const changeEmailControl = useDialogControl() 34 36 const birthdayControl = useDialogControl() 35 37 const changeHandleControl = useDialogControl() 36 38 const exportCarControl = useDialogControl() ··· 95 97 )} 96 98 <SettingsList.PressableItem 97 99 label={_(msg`Change email`)} 98 - onPress={() => openModal({name: 'change-email'})}> 100 + onPress={() => changeEmailControl.open()}> 99 101 <SettingsList.ItemIcon icon={PencilIcon} /> 100 102 <SettingsList.ItemText> 101 103 <Trans>Change email</Trans> ··· 165 167 </SettingsList.Container> 166 168 </Layout.Content> 167 169 168 - <VerifyEmailDialog control={verifyEmailControl} /> 170 + <ChangeEmailDialog 171 + control={changeEmailControl} 172 + verifyEmailControl={verifyEmailControl} 173 + /> 174 + <VerifyEmailDialog 175 + control={verifyEmailControl} 176 + changeEmailControl={changeEmailControl} 177 + /> 169 178 <BirthDateSettingsDialog control={birthdayControl} /> 170 179 <ChangeHandleDialog control={changeHandleControl} /> 171 180 <ExportCarDialog control={exportCarControl} />
+20 -1
src/screens/Settings/Settings.tsx
··· 3 3 import {Linking} from 'react-native' 4 4 import {useReducedMotion} from 'react-native-reanimated' 5 5 import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' 6 - import {msg, Trans} from '@lingui/macro' 6 + import {msg, t, Trans} from '@lingui/macro' 7 7 import {useLingui} from '@lingui/react' 8 8 import {useNavigation} from '@react-navigation/native' 9 9 import {type NativeStackScreenProps} from '@react-navigation/native-stack' ··· 18 18 import {sanitizeDisplayName} from '#/lib/strings/display-names' 19 19 import {sanitizeHandle} from '#/lib/strings/handles' 20 20 import {useProfileShadow} from '#/state/cache/profile-shadow' 21 + import * as persisted from '#/state/persisted' 21 22 import {clearStorage} from '#/state/persisted' 22 23 import {useModerationOpts} from '#/state/preferences/moderation-opts' 23 24 import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' ··· 359 360 Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) 360 361 } 361 362 363 + const onPressUnsnoozeReminder = () => { 364 + const lastEmailConfirm = new Date() 365 + // wind back 3 days 366 + lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3) 367 + persisted.write('reminders', { 368 + ...persisted.get('reminders'), 369 + lastEmailConfirm: lastEmailConfirm.toISOString(), 370 + }) 371 + Toast.show(t`You probably want to restart the app now.`) 372 + } 373 + 362 374 return ( 363 375 <> 364 376 <SettingsList.PressableItem ··· 394 406 label={_(msg`Reset onboarding state`)}> 395 407 <SettingsList.ItemText> 396 408 <Trans>Reset onboarding state</Trans> 409 + </SettingsList.ItemText> 410 + </SettingsList.PressableItem> 411 + <SettingsList.PressableItem 412 + onPress={onPressUnsnoozeReminder} 413 + label={_(msg`Unsnooze email reminder`)}> 414 + <SettingsList.ItemText> 415 + <Trans>Unsnooze email reminder</Trans> 397 416 </SettingsList.ItemText> 398 417 </SettingsList.PressableItem> 399 418 <SettingsList.PressableItem
+23 -7
src/screens/Settings/components/Email2FAToggle.tsx
··· 2 2 import {msg} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 - import {useModalControls} from '#/state/modals' 6 5 import {useAgent, useSession} from '#/state/session' 7 6 import {useDialogControl} from '#/components/Dialog' 7 + import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog' 8 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 8 9 import * as Prompt from '#/components/Prompt' 9 10 import {DisableEmail2FADialog} from './DisableEmail2FADialog' 10 11 import * as SettingsList from './SettingsList' ··· 12 13 export function Email2FAToggle() { 13 14 const {_} = useLingui() 14 15 const {currentAccount} = useSession() 15 - const {openModal} = useModalControls() 16 16 const disableDialogControl = useDialogControl() 17 17 const enableDialogControl = useDialogControl() 18 + const verifyEmailDialogControl = useDialogControl() 19 + const changeEmailDialogControl = useDialogControl() 18 20 const agent = useAgent() 19 21 20 22 const enableEmailAuthFactor = React.useCallback(async () => { ··· 35 37 disableDialogControl.open() 36 38 } else { 37 39 if (!currentAccount.emailConfirmed) { 38 - openModal({ 39 - name: 'verify-email', 40 - onSuccess: enableDialogControl.open, 41 - }) 40 + verifyEmailDialogControl.open() 42 41 return 43 42 } 44 43 enableDialogControl.open() 45 44 } 46 - }, [currentAccount, enableDialogControl, openModal, disableDialogControl]) 45 + }, [ 46 + currentAccount, 47 + enableDialogControl, 48 + verifyEmailDialogControl, 49 + disableDialogControl, 50 + ]) 47 51 48 52 return ( 49 53 <> ··· 54 58 description={_(msg`Require an email code to sign in to your account.`)} 55 59 onConfirm={enableEmailAuthFactor} 56 60 confirmButtonCta={_(msg`Enable`)} 61 + /> 62 + <VerifyEmailDialog 63 + control={verifyEmailDialogControl} 64 + changeEmailControl={changeEmailDialogControl} 65 + onCloseAfterVerifying={enableDialogControl.open} 66 + reasonText={_( 67 + msg`You need to verify your email address before you can enable email 2FA.`, 68 + )} 69 + /> 70 + <ChangeEmailDialog 71 + control={changeEmailDialogControl} 72 + verifyEmailControl={verifyEmailDialogControl} 57 73 /> 58 74 <SettingsList.BadgeButton 59 75 label={
-12
src/state/modals/index.tsx
··· 55 55 name: 'post-languages-settings' 56 56 } 57 57 58 - export interface VerifyEmailModal { 59 - name: 'verify-email' 60 - showReminder?: boolean 61 - onSuccess?: () => void 62 - } 63 - 64 - export interface ChangeEmailModal { 65 - name: 'change-email' 66 - } 67 - 68 58 export interface ChangePasswordModal { 69 59 name: 'change-password' 70 60 } ··· 84 74 export type Modal = 85 75 // Account 86 76 | DeleteAccountModal 87 - | VerifyEmailModal 88 - | ChangeEmailModal 89 77 | ChangePasswordModal 90 78 91 79 // Temp
-268
src/view/com/modals/ChangeEmail.tsx
··· 1 - import {useState} from 'react' 2 - import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {usePalette} from '#/lib/hooks/usePalette' 7 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 - import {cleanError} from '#/lib/strings/errors' 9 - import {colors, s} from '#/lib/styles' 10 - import {isWeb} from '#/platform/detection' 11 - import {useModalControls} from '#/state/modals' 12 - import {useAgent, useSession} from '#/state/session' 13 - import {ErrorMessage} from '../util/error/ErrorMessage' 14 - import {Button} from '../util/forms/Button' 15 - import {Text} from '../util/text/Text' 16 - import * as Toast from '../util/Toast' 17 - import {ScrollView, TextInput} from './util' 18 - 19 - enum Stages { 20 - InputEmail, 21 - ConfirmCode, 22 - Done, 23 - } 24 - 25 - export const snapPoints = ['90%'] 26 - 27 - export function Component() { 28 - const pal = usePalette('default') 29 - const {currentAccount} = useSession() 30 - const agent = useAgent() 31 - const {_} = useLingui() 32 - const [stage, setStage] = useState<Stages>(Stages.InputEmail) 33 - const [email, setEmail] = useState<string>(currentAccount?.email || '') 34 - const [confirmationCode, setConfirmationCode] = useState<string>('') 35 - const [isProcessing, setIsProcessing] = useState<boolean>(false) 36 - const [error, setError] = useState<string>('') 37 - const {isMobile} = useWebMediaQueries() 38 - const {openModal, closeModal} = useModalControls() 39 - 40 - const onRequestChange = async () => { 41 - if (email === currentAccount?.email) { 42 - setError(_(msg`Enter your new email above`)) 43 - return 44 - } 45 - setError('') 46 - setIsProcessing(true) 47 - try { 48 - const res = await agent.com.atproto.server.requestEmailUpdate() 49 - if (res.data.tokenRequired) { 50 - setStage(Stages.ConfirmCode) 51 - } else { 52 - await agent.com.atproto.server.updateEmail({email: email.trim()}) 53 - await agent.resumeSession(agent.session!) 54 - Toast.show(_(msg({message: 'Email updated', context: 'toast'}))) 55 - setStage(Stages.Done) 56 - } 57 - } catch (e) { 58 - let err = cleanError(String(e)) 59 - // TEMP 60 - // while rollout is occuring, we're giving a temporary error message 61 - // you can remove this any time after Oct2023 62 - // -prf 63 - if (err === 'email must be confirmed (temporary)') { 64 - err = _( 65 - msg`Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`, 66 - ) 67 - } 68 - setError(err) 69 - } finally { 70 - setIsProcessing(false) 71 - } 72 - } 73 - 74 - const onConfirm = async () => { 75 - setError('') 76 - setIsProcessing(true) 77 - try { 78 - await agent.com.atproto.server.updateEmail({ 79 - email: email.trim(), 80 - token: confirmationCode.trim(), 81 - }) 82 - await agent.resumeSession(agent.session!) 83 - Toast.show(_(msg({message: 'Email updated', context: 'toast'}))) 84 - setStage(Stages.Done) 85 - } catch (e) { 86 - setError(cleanError(String(e))) 87 - } finally { 88 - setIsProcessing(false) 89 - } 90 - } 91 - 92 - const onVerify = async () => { 93 - closeModal() 94 - openModal({name: 'verify-email'}) 95 - } 96 - 97 - return ( 98 - <SafeAreaView style={[pal.view, s.flex1]}> 99 - <ScrollView 100 - testID="changeEmailModal" 101 - style={[s.flex1, isMobile && {paddingHorizontal: 18}]}> 102 - <View style={styles.titleSection}> 103 - <Text type="title-lg" style={[pal.text, styles.title]}> 104 - {stage === Stages.InputEmail ? _(msg`Change Your Email`) : ''} 105 - {stage === Stages.ConfirmCode ? _(msg`Security Step Required`) : ''} 106 - {stage === Stages.Done ? _(msg`Email Updated`) : ''} 107 - </Text> 108 - </View> 109 - 110 - <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> 111 - {stage === Stages.InputEmail ? ( 112 - <Trans>Enter your new email address below.</Trans> 113 - ) : stage === Stages.ConfirmCode ? ( 114 - <Trans> 115 - An email has been sent to your previous address,{' '} 116 - {currentAccount?.email || '(no email)'}. It includes a 117 - confirmation code which you can enter below. 118 - </Trans> 119 - ) : ( 120 - <Trans> 121 - Your email has been updated but not verified. As a next step, 122 - please verify your new email. 123 - </Trans> 124 - )} 125 - </Text> 126 - 127 - {stage === Stages.InputEmail && ( 128 - <TextInput 129 - testID="emailInput" 130 - style={[styles.textInput, pal.border, pal.text]} 131 - placeholder="alice@mail.com" 132 - placeholderTextColor={pal.colors.textLight} 133 - value={email} 134 - onChangeText={setEmail} 135 - accessible={true} 136 - accessibilityLabel={_(msg`Email`)} 137 - accessibilityHint="" 138 - autoCapitalize="none" 139 - autoComplete="email" 140 - autoCorrect={false} 141 - /> 142 - )} 143 - {stage === Stages.ConfirmCode && ( 144 - <TextInput 145 - testID="confirmCodeInput" 146 - style={[styles.textInput, pal.border, pal.text]} 147 - placeholder="XXXXX-XXXXX" 148 - placeholderTextColor={pal.colors.textLight} 149 - value={confirmationCode} 150 - onChangeText={setConfirmationCode} 151 - accessible={true} 152 - accessibilityLabel={_(msg`Confirmation code`)} 153 - accessibilityHint="" 154 - autoCapitalize="none" 155 - autoComplete="off" 156 - autoCorrect={false} 157 - /> 158 - )} 159 - 160 - {error ? ( 161 - <ErrorMessage message={error} style={styles.error} /> 162 - ) : undefined} 163 - 164 - <View style={[styles.btnContainer]}> 165 - {isProcessing ? ( 166 - <View style={styles.btn}> 167 - <ActivityIndicator color="#fff" /> 168 - </View> 169 - ) : ( 170 - <View style={{gap: 6}}> 171 - {stage === Stages.InputEmail && ( 172 - <Button 173 - testID="requestChangeBtn" 174 - type="primary" 175 - onPress={onRequestChange} 176 - accessibilityLabel={_(msg`Request Change`)} 177 - accessibilityHint="" 178 - label={_(msg`Request Change`)} 179 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 180 - labelStyle={[s.f18]} 181 - /> 182 - )} 183 - {stage === Stages.ConfirmCode && ( 184 - <Button 185 - testID="confirmBtn" 186 - type="primary" 187 - onPress={onConfirm} 188 - accessibilityLabel={_(msg`Confirm Change`)} 189 - accessibilityHint="" 190 - label={_(msg`Confirm Change`)} 191 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 192 - labelStyle={[s.f18]} 193 - /> 194 - )} 195 - {stage === Stages.Done && ( 196 - <Button 197 - testID="verifyBtn" 198 - type="primary" 199 - onPress={onVerify} 200 - accessibilityLabel={_(msg`Verify New Email`)} 201 - accessibilityHint="" 202 - label={_(msg`Verify New Email`)} 203 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 204 - labelStyle={[s.f18]} 205 - /> 206 - )} 207 - <Button 208 - testID="cancelBtn" 209 - type="default" 210 - onPress={() => { 211 - closeModal() 212 - }} 213 - accessibilityLabel={_(msg`Cancel`)} 214 - accessibilityHint="" 215 - label={_(msg`Cancel`)} 216 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 217 - labelStyle={[s.f18]} 218 - /> 219 - </View> 220 - )} 221 - </View> 222 - </ScrollView> 223 - </SafeAreaView> 224 - ) 225 - } 226 - 227 - const styles = StyleSheet.create({ 228 - titleSection: { 229 - paddingTop: isWeb ? 0 : 4, 230 - paddingBottom: isWeb ? 14 : 10, 231 - }, 232 - title: { 233 - textAlign: 'center', 234 - fontWeight: '600', 235 - marginBottom: 5, 236 - }, 237 - error: { 238 - borderRadius: 6, 239 - marginTop: 10, 240 - }, 241 - emailContainer: { 242 - flexDirection: 'row', 243 - alignItems: 'center', 244 - gap: 6, 245 - borderWidth: 1, 246 - borderRadius: 6, 247 - paddingHorizontal: 14, 248 - paddingVertical: 12, 249 - }, 250 - textInput: { 251 - borderWidth: 1, 252 - borderRadius: 6, 253 - paddingHorizontal: 14, 254 - paddingVertical: 10, 255 - fontSize: 16, 256 - }, 257 - btn: { 258 - flexDirection: 'row', 259 - alignItems: 'center', 260 - justifyContent: 'center', 261 - borderRadius: 32, 262 - padding: 14, 263 - backgroundColor: colors.blue3, 264 - }, 265 - btnContainer: { 266 - paddingTop: 20, 267 - }, 268 - })
-8
src/view/com/modals/Modal.tsx
··· 7 7 import {useModalControls, useModals} from '#/state/modals' 8 8 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 9 9 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 10 - import * as ChangeEmailModal from './ChangeEmail' 11 10 import * as ChangePasswordModal from './ChangePassword' 12 11 import * as CreateOrEditListModal from './CreateOrEditList' 13 12 import * as DeleteAccountModal from './DeleteAccount' ··· 18 17 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' 19 18 import * as LinkWarningModal from './LinkWarning' 20 19 import * as UserAddRemoveListsModal from './UserAddRemoveLists' 21 - import * as VerifyEmailModal from './VerifyEmail' 22 20 23 21 const DEFAULT_SNAPPOINTS = ['90%'] 24 22 const HANDLE_HEIGHT = 24 ··· 72 70 } else if (activeModal?.name === 'post-languages-settings') { 73 71 snapPoints = PostLanguagesSettingsModal.snapPoints 74 72 element = <PostLanguagesSettingsModal.Component /> 75 - } else if (activeModal?.name === 'verify-email') { 76 - snapPoints = VerifyEmailModal.snapPoints 77 - element = <VerifyEmailModal.Component {...activeModal} /> 78 - } else if (activeModal?.name === 'change-email') { 79 - snapPoints = ChangeEmailModal.snapPoints 80 - element = <ChangeEmailModal.Component /> 81 73 } else if (activeModal?.name === 'change-password') { 82 74 snapPoints = ChangePasswordModal.snapPoints 83 75 element = <ChangePasswordModal.Component />
-6
src/view/com/modals/Modal.web.tsx
··· 6 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 7 import {type Modal as ModalIface} from '#/state/modals' 8 8 import {useModalControls, useModals} from '#/state/modals' 9 - import * as ChangeEmailModal from './ChangeEmail' 10 9 import * as ChangePasswordModal from './ChangePassword' 11 10 import * as CreateOrEditListModal from './CreateOrEditList' 12 11 import * as CropImageModal from './CropImage.web' ··· 17 16 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' 18 17 import * as LinkWarningModal from './LinkWarning' 19 18 import * as UserAddRemoveLists from './UserAddRemoveLists' 20 - import * as VerifyEmailModal from './VerifyEmail' 21 19 22 20 export function ModalsContainer() { 23 21 const {isModalActive, activeModals} = useModals() ··· 74 72 element = <ContentLanguagesSettingsModal.Component /> 75 73 } else if (modal.name === 'post-languages-settings') { 76 74 element = <PostLanguagesSettingsModal.Component /> 77 - } else if (modal.name === 'verify-email') { 78 - element = <VerifyEmailModal.Component {...modal} /> 79 - } else if (modal.name === 'change-email') { 80 - element = <ChangeEmailModal.Component /> 81 75 } else if (modal.name === 'change-password') { 82 76 element = <ChangePasswordModal.Component /> 83 77 } else if (modal.name === 'link-warning') {
-342
src/view/com/modals/VerifyEmail.tsx
··· 1 - import React, {useState} from 'react' 2 - import { 3 - ActivityIndicator, 4 - Pressable, 5 - SafeAreaView, 6 - StyleSheet, 7 - View, 8 - } from 'react-native' 9 - import {Circle, Path, Svg} from 'react-native-svg' 10 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 - import {msg, Trans} from '@lingui/macro' 12 - import {useLingui} from '@lingui/react' 13 - 14 - import {usePalette} from '#/lib/hooks/usePalette' 15 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 16 - import {cleanError} from '#/lib/strings/errors' 17 - import {colors, s} from '#/lib/styles' 18 - import {logger} from '#/logger' 19 - import {isWeb} from '#/platform/detection' 20 - import {useModalControls} from '#/state/modals' 21 - import {useAgent, useSession} from '#/state/session' 22 - import {ErrorMessage} from '../util/error/ErrorMessage' 23 - import {Button} from '../util/forms/Button' 24 - import {Text} from '../util/text/Text' 25 - import * as Toast from '../util/Toast' 26 - import {ScrollView, TextInput} from './util' 27 - 28 - export const snapPoints = ['90%'] 29 - 30 - enum Stages { 31 - Reminder, 32 - Email, 33 - ConfirmCode, 34 - } 35 - 36 - export function Component({ 37 - showReminder, 38 - onSuccess, 39 - }: { 40 - showReminder?: boolean 41 - onSuccess?: () => void 42 - }) { 43 - const pal = usePalette('default') 44 - const agent = useAgent() 45 - const {currentAccount} = useSession() 46 - const {_} = useLingui() 47 - const [stage, setStage] = useState<Stages>( 48 - showReminder ? Stages.Reminder : Stages.Email, 49 - ) 50 - const [confirmationCode, setConfirmationCode] = useState<string>('') 51 - const [isProcessing, setIsProcessing] = useState<boolean>(false) 52 - const [error, setError] = useState<string>('') 53 - const {isMobile} = useWebMediaQueries() 54 - const {openModal, closeModal} = useModalControls() 55 - 56 - React.useEffect(() => { 57 - if (!currentAccount) { 58 - logger.error(`VerifyEmail modal opened without currentAccount`) 59 - closeModal() 60 - } 61 - }, [currentAccount, closeModal]) 62 - 63 - const onSendEmail = async () => { 64 - setError('') 65 - setIsProcessing(true) 66 - try { 67 - await agent.com.atproto.server.requestEmailConfirmation() 68 - setStage(Stages.ConfirmCode) 69 - } catch (e) { 70 - setError(cleanError(String(e))) 71 - } finally { 72 - setIsProcessing(false) 73 - } 74 - } 75 - 76 - const onConfirm = async () => { 77 - setError('') 78 - setIsProcessing(true) 79 - try { 80 - await agent.com.atproto.server.confirmEmail({ 81 - email: (currentAccount?.email || '').trim(), 82 - token: confirmationCode.trim(), 83 - }) 84 - await agent.resumeSession(agent.session!) 85 - Toast.show(_(msg({message: 'Email verified', context: 'toast'}))) 86 - closeModal() 87 - onSuccess?.() 88 - } catch (e) { 89 - setError(cleanError(String(e))) 90 - } finally { 91 - setIsProcessing(false) 92 - } 93 - } 94 - 95 - const onEmailIncorrect = () => { 96 - closeModal() 97 - openModal({name: 'change-email'}) 98 - } 99 - 100 - return ( 101 - <SafeAreaView style={[pal.view, s.flex1]}> 102 - <ScrollView 103 - testID="verifyEmailModal" 104 - style={[s.flex1, isMobile && {paddingHorizontal: 18}]}> 105 - {stage === Stages.Reminder && <ReminderIllustration />} 106 - <View style={styles.titleSection}> 107 - <Text type="title-lg" style={[pal.text, styles.title]}> 108 - {stage === Stages.Reminder ? ( 109 - <Trans>Please Verify Your Email</Trans> 110 - ) : stage === Stages.Email ? ( 111 - <Trans>Verify Your Email</Trans> 112 - ) : stage === Stages.ConfirmCode ? ( 113 - <Trans>Enter Confirmation Code</Trans> 114 - ) : ( 115 - '' 116 - )} 117 - </Text> 118 - </View> 119 - 120 - <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> 121 - {stage === Stages.Reminder ? ( 122 - <Trans> 123 - Your email has not yet been verified. This is an important 124 - security step which we recommend. 125 - </Trans> 126 - ) : stage === Stages.Email ? ( 127 - <Trans> 128 - This is important in case you ever need to change your email or 129 - reset your password. 130 - </Trans> 131 - ) : stage === Stages.ConfirmCode ? ( 132 - <Trans> 133 - An email has been sent to {currentAccount?.email || '(no email)'}. 134 - It includes a confirmation code which you can enter below. 135 - </Trans> 136 - ) : ( 137 - '' 138 - )} 139 - </Text> 140 - 141 - {stage === Stages.Email ? ( 142 - <> 143 - <View style={styles.emailContainer}> 144 - <FontAwesomeIcon 145 - icon="envelope" 146 - color={pal.colors.text} 147 - size={16} 148 - /> 149 - <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}> 150 - {currentAccount?.email || _(msg`(no email)`)} 151 - </Text> 152 - </View> 153 - <Pressable 154 - accessibilityRole="link" 155 - accessibilityLabel={_(msg`Change my email`)} 156 - accessibilityHint="" 157 - onPress={onEmailIncorrect} 158 - style={styles.changeEmailLink}> 159 - <Text type="lg" style={pal.link}> 160 - <Trans>Change</Trans> 161 - </Text> 162 - </Pressable> 163 - </> 164 - ) : stage === Stages.ConfirmCode ? ( 165 - <TextInput 166 - testID="confirmCodeInput" 167 - style={[styles.textInput, pal.border, pal.text]} 168 - placeholder="XXXXX-XXXXX" 169 - placeholderTextColor={pal.colors.textLight} 170 - value={confirmationCode} 171 - onChangeText={setConfirmationCode} 172 - accessible={true} 173 - accessibilityLabel={_(msg`Confirmation code`)} 174 - accessibilityHint="" 175 - autoCapitalize="none" 176 - autoComplete="one-time-code" 177 - autoCorrect={false} 178 - /> 179 - ) : undefined} 180 - 181 - {error ? ( 182 - <ErrorMessage message={error} style={styles.error} /> 183 - ) : undefined} 184 - 185 - <View style={[styles.btnContainer]}> 186 - {isProcessing ? ( 187 - <View style={styles.btn}> 188 - <ActivityIndicator color="#fff" /> 189 - </View> 190 - ) : ( 191 - <View style={{gap: 6}}> 192 - {stage === Stages.Reminder && ( 193 - <Button 194 - testID="getStartedBtn" 195 - type="primary" 196 - onPress={() => setStage(Stages.Email)} 197 - accessibilityLabel={_(msg`Get Started`)} 198 - accessibilityHint="" 199 - label={_(msg`Get Started`)} 200 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 201 - labelStyle={[s.f18]} 202 - /> 203 - )} 204 - {stage === Stages.Email && ( 205 - <> 206 - <Button 207 - testID="sendEmailBtn" 208 - type="primary" 209 - onPress={onSendEmail} 210 - accessibilityLabel={_(msg`Send Confirmation Email`)} 211 - accessibilityHint="" 212 - label={_(msg`Send Confirmation Email`)} 213 - labelContainerStyle={{ 214 - justifyContent: 'center', 215 - padding: 4, 216 - }} 217 - labelStyle={[s.f18]} 218 - /> 219 - <Button 220 - testID="haveCodeBtn" 221 - type="default" 222 - accessibilityLabel={_(msg`I have a code`)} 223 - accessibilityHint="" 224 - label={_(msg`I have a confirmation code`)} 225 - labelContainerStyle={{ 226 - justifyContent: 'center', 227 - padding: 4, 228 - }} 229 - labelStyle={[s.f18]} 230 - onPress={() => setStage(Stages.ConfirmCode)} 231 - /> 232 - </> 233 - )} 234 - {stage === Stages.ConfirmCode && ( 235 - <Button 236 - testID="confirmBtn" 237 - type="primary" 238 - onPress={onConfirm} 239 - accessibilityLabel={_(msg`Confirm`)} 240 - accessibilityHint="" 241 - label={_(msg`Confirm`)} 242 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 243 - labelStyle={[s.f18]} 244 - /> 245 - )} 246 - <Button 247 - testID="cancelBtn" 248 - type="default" 249 - onPress={() => { 250 - closeModal() 251 - }} 252 - accessibilityLabel={ 253 - stage === Stages.Reminder 254 - ? _(msg`Not right now`) 255 - : _(msg`Cancel`) 256 - } 257 - accessibilityHint="" 258 - label={ 259 - stage === Stages.Reminder 260 - ? _(msg`Not right now`) 261 - : _(msg`Cancel`) 262 - } 263 - labelContainerStyle={{justifyContent: 'center', padding: 4}} 264 - labelStyle={[s.f18]} 265 - /> 266 - </View> 267 - )} 268 - </View> 269 - </ScrollView> 270 - </SafeAreaView> 271 - ) 272 - } 273 - 274 - function ReminderIllustration() { 275 - const pal = usePalette('default') 276 - const palInverted = usePalette('inverted') 277 - return ( 278 - <View style={[pal.viewLight, {borderRadius: 8, marginBottom: 20}]}> 279 - <Svg viewBox="0 0 112 84" fill="none" height={200}> 280 - <Path 281 - fillRule="evenodd" 282 - clipRule="evenodd" 283 - d="M26 26.4264V55C26 60.5229 30.4772 65 36 65H76C81.5228 65 86 60.5229 86 55V27.4214L63.5685 49.8528C59.6633 53.7581 53.3316 53.7581 49.4264 49.8528L26 26.4264Z" 284 - fill={palInverted.colors.background} 285 - /> 286 - <Path 287 - fillRule="evenodd" 288 - clipRule="evenodd" 289 - d="M83.666 19.5784C85.47 21.7297 84.4897 24.7895 82.5044 26.7748L60.669 48.6102C58.3259 50.9533 54.5269 50.9533 52.1838 48.6102L29.9502 26.3766C27.8241 24.2505 26.8952 20.8876 29.0597 18.8005C30.8581 17.0665 33.3045 16 36 16H76C79.0782 16 81.8316 17.3908 83.666 19.5784Z" 290 - fill={palInverted.colors.background} 291 - /> 292 - <Circle cx="82" cy="61" r="13" fill="#20BC07" /> 293 - <Path d="M75 61L80 66L89 57" stroke="white" strokeWidth="2" /> 294 - </Svg> 295 - </View> 296 - ) 297 - } 298 - 299 - const styles = StyleSheet.create({ 300 - titleSection: { 301 - paddingTop: isWeb ? 0 : 4, 302 - paddingBottom: isWeb ? 14 : 10, 303 - }, 304 - title: { 305 - textAlign: 'center', 306 - fontWeight: '600', 307 - marginBottom: 5, 308 - }, 309 - error: { 310 - borderRadius: 6, 311 - marginTop: 10, 312 - }, 313 - emailContainer: { 314 - flexDirection: 'row', 315 - alignItems: 'center', 316 - gap: 6, 317 - paddingHorizontal: 14, 318 - marginTop: 10, 319 - }, 320 - changeEmailLink: { 321 - marginHorizontal: 12, 322 - marginBottom: 12, 323 - }, 324 - textInput: { 325 - borderWidth: 1, 326 - borderRadius: 6, 327 - paddingHorizontal: 14, 328 - paddingVertical: 10, 329 - fontSize: 16, 330 - }, 331 - btn: { 332 - flexDirection: 'row', 333 - alignItems: 'center', 334 - justifyContent: 'center', 335 - borderRadius: 32, 336 - padding: 14, 337 - backgroundColor: colors.blue3, 338 - }, 339 - btnContainer: { 340 - paddingTop: 20, 341 - }, 342 - })