diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-04-23 19:22:08 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-23 11:22:08 -0500 |
commit | 70dbc94766b8f3c9d2c1b815fad66232523d28ab (patch) | |
tree | 6c860d092c29b48f6dda9c58364f78a8ef07de2c /src | |
parent | 118d385b1010190542e58fba1d640f75714b6ea9 (diff) | |
download | voidsky-70dbc94766b8f3c9d2c1b815fad66232523d28ab.tar.zst |
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>
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 72 | ||||
-rw-r--r-- | src/components/dialogs/ChangeEmailDialog.tsx | 259 | ||||
-rw-r--r-- | src/components/dialogs/VerifyEmailDialog.tsx | 159 | ||||
-rw-r--r-- | src/screens/Settings/AccountSettings.tsx | 13 | ||||
-rw-r--r-- | src/screens/Settings/Settings.tsx | 21 | ||||
-rw-r--r-- | src/screens/Settings/components/Email2FAToggle.tsx | 30 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 12 | ||||
-rw-r--r-- | src/view/com/modals/ChangeEmail.tsx | 268 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 8 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/VerifyEmail.tsx | 342 |
11 files changed, 462 insertions, 728 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index d4fdc4797..424d73290 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -33,7 +33,6 @@ import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig' import {bskyTitle} from '#/lib/strings/headings' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useSession} from '#/state/session' import { @@ -80,33 +79,35 @@ import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed' import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers' import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' +import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch' import {SearchScreen} from '#/screens/Search' +import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings' +import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' +import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' +import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' +import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' +import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' +import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' +import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' +import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' +import {SettingsScreen} from '#/screens/Settings/Settings' import {SettingsInterests} from '#/screens/Settings/SettingsInterests' +import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { StarterPackScreen, StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen' import {Wizard} from '#/screens/StarterPack/Wizard' +import TopicScreen from '#/screens/Topic' import {VideoFeed} from '#/screens/VideoFeed' import {useTheme} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' -import {ProfileSearchScreen} from './screens/Profile/ProfileSearch' -import {AboutSettingsScreen} from './screens/Settings/AboutSettings' -import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' -import {AccountSettingsScreen} from './screens/Settings/AccountSettings' -import {AppPasswordsScreen} from './screens/Settings/AppPasswords' -import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' -import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' -import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' -import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' -import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' -import {SettingsScreen} from './screens/Settings/Settings' -import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' -import TopicScreen from './screens/Topic' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() @@ -736,36 +737,39 @@ const LINKING = { function RoutesContainer({children}: React.PropsWithChildren<{}>) { const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) const {currentAccount} = useSession() - const {openModal} = useModalControls() const prevLoggedRouteName = React.useRef<string | undefined>(undefined) + const verifyEmailDialogControl = useDialogControl() function onReady() { prevLoggedRouteName.current = getCurrentRouteName() if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { - openModal({name: 'verify-email', showReminder: true}) + verifyEmailDialogControl.open() snoozeEmailConfirmationPrompt() } } return ( - <NavigationContainer - ref={navigationRef} - linking={LINKING} - theme={theme} - onStateChange={() => { - logger.metric('router:navigate', { - from: prevLoggedRouteName.current, - }) - prevLoggedRouteName.current = getCurrentRouteName() - }} - onReady={() => { - attachRouteToLogEvents(getCurrentRouteName) - logModuleInitTime() - onReady() - logger.metric('router:navigate', {}) - }}> - {children} - </NavigationContainer> + <> + <NavigationContainer + ref={navigationRef} + linking={LINKING} + theme={theme} + onStateChange={() => { + logger.metric('router:navigate', { + from: prevLoggedRouteName.current, + }) + prevLoggedRouteName.current = getCurrentRouteName() + }} + onReady={() => { + attachRouteToLogEvents(getCurrentRouteName) + logModuleInitTime() + onReady() + logger.metric('router:navigate', {}) + }}> + {children} + </NavigationContainer> + <VerifyEmailDialog control={verifyEmailDialogControl} reminder /> + </> ) } diff --git a/src/components/dialogs/ChangeEmailDialog.tsx b/src/components/dialogs/ChangeEmailDialog.tsx new file mode 100644 index 000000000..93397bae9 --- /dev/null +++ b/src/components/dialogs/ChangeEmailDialog.tsx @@ -0,0 +1,259 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {useAgent, useSession} from '#/state/session' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import {atoms as a, useBreakpoints, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export function ChangeEmailDialog({ + control, + verifyEmailControl, +}: { + control: Dialog.DialogControlProps + verifyEmailControl: Dialog.DialogControlProps +}) { + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + <Inner verifyEmailControl={verifyEmailControl} /> + </Dialog.Outer> + ) +} + +export function Inner({ + verifyEmailControl, +}: { + verifyEmailControl: Dialog.DialogControlProps +}) { + const {_} = useLingui() + const {currentAccount} = useSession() + const agent = useAgent() + const control = Dialog.useDialogContext() + const {gtMobile} = useBreakpoints() + + const [currentStep, setCurrentStep] = useState< + 'StepOne' | 'StepTwo' | 'StepThree' + >('StepOne') + const [email, setEmail] = useState('') + const [confirmationCode, setConfirmationCode] = useState('') + const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') + + const currentEmail = currentAccount?.email || '(no email)' + const uiStrings = { + StepOne: { + title: _(msg`Change Your Email`), + message: '', + }, + StepTwo: { + title: _(msg`Security Step Required`), + message: _( + msg`An email has been sent to your previous address, ${currentEmail}. It includes a confirmation code which you can enter below.`, + ), + }, + StepThree: { + title: _(msg`Email Updated!`), + message: _( + msg`Your email address has been updated but it is not yet verified. As a next step, please verify your new email.`, + ), + }, + } + + const onRequestChange = async () => { + if (email === currentAccount?.email) { + setError( + _( + msg`The email address you entered is the same as your current email address.`, + ), + ) + return + } + setError('') + setIsProcessing(true) + try { + const res = await agent.com.atproto.server.requestEmailUpdate() + if (res.data.tokenRequired) { + setCurrentStep('StepTwo') + } else { + await agent.com.atproto.server.updateEmail({email: email.trim()}) + await agent.resumeSession(agent.session!) + setCurrentStep('StepThree') + } + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onConfirm = async () => { + setError('') + setIsProcessing(true) + try { + await agent.com.atproto.server.updateEmail({ + email: email.trim(), + token: confirmationCode.trim(), + }) + await agent.resumeSession(agent.session!) + setCurrentStep('StepThree') + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onVerify = async () => { + control.close(() => { + verifyEmailControl.open() + }) + } + + return ( + <Dialog.ScrollableInner + label={_(msg`Verify email dialog`)} + style={web({maxWidth: 450})}> + <Dialog.Close /> + <View style={[a.gap_xl]}> + <View style={[a.gap_sm]}> + <Text style={[a.font_heavy, a.text_2xl]}> + {uiStrings[currentStep].title} + </Text> + {error ? ( + <View style={[a.rounded_sm, a.overflow_hidden]}> + <ErrorMessage message={error} /> + </View> + ) : null} + {currentStep === 'StepOne' ? ( + <View> + <TextField.LabelText> + <Trans>Enter your new email address below.</Trans> + </TextField.LabelText> + <TextField.Root> + <TextField.Input + label={_(msg`New email address`)} + placeholder={_(msg`alice@example.com`)} + defaultValue={email} + onChangeText={setEmail} + keyboardType="email-address" + autoComplete="email" + /> + </TextField.Root> + </View> + ) : ( + <Text style={[a.text_md, a.leading_snug]}> + {uiStrings[currentStep].message} + </Text> + )} + </View> + {currentStep === 'StepTwo' ? ( + <View> + <TextField.LabelText> + <Trans>Confirmation code</Trans> + </TextField.LabelText> + <TextField.Root> + <TextField.Input + label={_(msg`Confirmation code`)} + placeholder="XXXXX-XXXXX" + onChangeText={setConfirmationCode} + /> + </TextField.Root> + </View> + ) : null} + <View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}> + {currentStep === 'StepOne' ? ( + <> + <Button + label={_(msg`Request change`)} + variant="solid" + color="primary" + size="large" + disabled={isProcessing} + onPress={onRequestChange}> + <ButtonText> + <Trans>Request change</Trans> + </ButtonText> + {isProcessing ? ( + <Loader size="sm" style={[{color: 'white'}]} /> + ) : null} + </Button> + <Button + label={_(msg`I have a code`)} + variant="solid" + color="secondary" + size="large" + disabled={isProcessing} + onPress={() => setCurrentStep('StepTwo')}> + <ButtonText> + <Trans>I have a code</Trans> + </ButtonText> + </Button> + </> + ) : currentStep === 'StepTwo' ? ( + <> + <Button + label={_(msg`Confirm`)} + variant="solid" + color="primary" + size="large" + disabled={isProcessing} + onPress={onConfirm}> + <ButtonText> + <Trans>Confirm</Trans> + </ButtonText> + {isProcessing ? ( + <Loader size="sm" style={[{color: 'white'}]} /> + ) : null} + </Button> + <Button + label={_(msg`Resend email`)} + variant="solid" + color="secondary" + size="large" + disabled={isProcessing} + onPress={() => { + setConfirmationCode('') + setCurrentStep('StepOne') + }}> + <ButtonText> + <Trans>Resend email</Trans> + </ButtonText> + </Button> + </> + ) : currentStep === 'StepThree' ? ( + <> + <Button + label={_(msg`Verify email`)} + variant="solid" + color="primary" + size="large" + onPress={onVerify}> + <ButtonText> + <Trans>Verify email</Trans> + </ButtonText> + </Button> + <Button + label={_(msg`Close`)} + variant="solid" + color="secondary" + size="large" + onPress={() => control.close()}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + </> + ) : null} + </View> + </View> + </Dialog.ScrollableInner> + ) +} diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx index ced9171ce..b8d1cd192 100644 --- a/src/components/dialogs/VerifyEmailDialog.tsx +++ b/src/components/dialogs/VerifyEmailDialog.tsx @@ -1,86 +1,115 @@ -import React from 'react' +import {useState} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' import {useAgent, useSession} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {atoms as a, useBreakpoints} from '#/alf' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' +import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' +import {ChangeEmailDialog} from './ChangeEmailDialog' export function VerifyEmailDialog({ control, onCloseWithoutVerifying, onCloseAfterVerifying, reasonText, + changeEmailControl, + reminder, }: { control: Dialog.DialogControlProps onCloseWithoutVerifying?: () => void onCloseAfterVerifying?: () => void reasonText?: string + /** + * if a changeEmailControl for a ChangeEmailDialog is not provided, + * this component will create one for you. Using this prop + * helps reduce duplication, since these dialogs are often used together. + */ + changeEmailControl?: Dialog.DialogControlProps + reminder?: boolean }) { const agent = useAgent() + const fallbackChangeEmailControl = Dialog.useDialogControl() - const [didVerify, setDidVerify] = React.useState(false) + const [didVerify, setDidVerify] = useState(false) return ( - <Dialog.Outer - control={control} - onClose={async () => { - if (!didVerify) { - onCloseWithoutVerifying?.() - return - } - - try { - await agent.resumeSession(agent.session!) - onCloseAfterVerifying?.() - } catch (e: unknown) { - logger.error(String(e)) - return - } - }}> - <Dialog.Handle /> - <Inner + <> + <Dialog.Outer control={control} - setDidVerify={setDidVerify} - reasonText={reasonText} - /> - </Dialog.Outer> + onClose={async () => { + if (!didVerify) { + onCloseWithoutVerifying?.() + return + } + + try { + await agent.resumeSession(agent.session!) + onCloseAfterVerifying?.() + } catch (e: unknown) { + logger.error(String(e)) + return + } + }}> + <Dialog.Handle /> + <Inner + setDidVerify={setDidVerify} + reasonText={reasonText} + changeEmailControl={changeEmailControl ?? fallbackChangeEmailControl} + reminder={reminder} + /> + </Dialog.Outer> + {!changeEmailControl && ( + <ChangeEmailDialog + control={fallbackChangeEmailControl} + verifyEmailControl={control} + /> + )} + </> ) } export function Inner({ - control, setDidVerify, reasonText, + changeEmailControl, + reminder, }: { - control: Dialog.DialogControlProps setDidVerify: (value: boolean) => void reasonText?: string + changeEmailControl: Dialog.DialogControlProps + reminder?: boolean }) { + const control = Dialog.useDialogContext() const {_} = useLingui() const {currentAccount} = useSession() const agent = useAgent() - const {openModal} = useModalControls() const {gtMobile} = useBreakpoints() + const t = useTheme() - const [currentStep, setCurrentStep] = React.useState< - 'StepOne' | 'StepTwo' | 'StepThree' - >('StepOne') - const [confirmationCode, setConfirmationCode] = React.useState('') - const [isProcessing, setIsProcessing] = React.useState(false) - const [error, setError] = React.useState('') + const [currentStep, setCurrentStep] = useState< + 'Reminder' | 'StepOne' | 'StepTwo' | 'StepThree' + >(reminder ? 'Reminder' : 'StepOne') + const [confirmationCode, setConfirmationCode] = useState('') + const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') const uiStrings = { + Reminder: { + title: _(msg`Please Verify Your Email`), + message: _( + msg`Your email has not yet been verified. This is an important security step which we recommend.`, + ), + }, StepOne: { title: _(msg`Verify Your Email`), message: '', @@ -132,11 +161,20 @@ export function Inner({ return ( <Dialog.ScrollableInner label={_(msg`Verify email dialog`)} - style={[ - gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, - ]}> - <Dialog.Close /> + style={web({maxWidth: 450})}> <View style={[a.gap_xl]}> + {currentStep === 'Reminder' && ( + <View + style={[ + a.rounded_sm, + a.align_center, + a.justify_center, + {height: 150}, + t.atoms.bg_contrast_100, + ]}> + <EnvelopeIcon width={64} fill="white" /> + </View> + )} <View style={[a.gap_sm]}> <Text style={[a.font_heavy, a.text_2xl]}> {uiStrings[currentStep].title} @@ -164,7 +202,7 @@ export function Inner({ onPress={e => { e.preventDefault() control.close(() => { - openModal({name: 'change-email'}) + changeEmailControl.open() }) return false }}> @@ -189,7 +227,7 @@ export function Inner({ onPress={e => { e.preventDefault() control.close(() => { - openModal({name: 'change-email'}) + changeEmailControl.open() }) return false }}> @@ -219,7 +257,32 @@ export function Inner({ </View> ) : null} <View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}> - {currentStep === 'StepOne' ? ( + {currentStep === 'Reminder' ? ( + <> + <Button + label={_(msg`Get started`)} + variant="solid" + color="primary" + size="large" + onPress={() => setCurrentStep('StepOne')}> + <ButtonText> + <Trans>Get started</Trans> + </ButtonText> + </Button> + <Button + label={_(msg`Maybe later`)} + accessibilityHint={_(msg`Snoozes the reminder`)} + variant="ghost" + color="secondary" + size="large" + disabled={isProcessing} + onPress={() => control.close()}> + <ButtonText> + <Trans>Maybe later</Trans> + </ButtonText> + </Button> + </> + ) : currentStep === 'StepOne' ? ( <> <Button label={_(msg`Send confirmation email`)} @@ -229,21 +292,21 @@ export function Inner({ disabled={isProcessing} onPress={onSendEmail}> <ButtonText> - <Trans>Send Confirmation</Trans> + <Trans>Send confirmation</Trans> </ButtonText> {isProcessing ? ( <Loader size="sm" style={[{color: 'white'}]} /> ) : null} </Button> <Button - label={_(msg`I Have a Code`)} + label={_(msg`I have a code`)} variant="solid" color="secondary" size="large" disabled={isProcessing} onPress={() => setCurrentStep('StepTwo')}> <ButtonText> - <Trans>I Have a Code</Trans> + <Trans>I have a code</Trans> </ButtonText> </Button> </> @@ -264,7 +327,7 @@ export function Inner({ ) : null} </Button> <Button - label={_(msg`Resend Email`)} + label={_(msg`Resend email`)} variant="solid" color="secondary" size="large" @@ -274,13 +337,13 @@ export function Inner({ setCurrentStep('StepOne') }}> <ButtonText> - <Trans>Resend Email</Trans> + <Trans>Resend email</Trans> </ButtonText> </Button> </> ) : currentStep === 'StepThree' ? ( <Button - label={_(msg`Confirm`)} + label={_(msg`Close`)} variant="solid" color="primary" size="large" diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index a69c5cdd3..7c50bd8df 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -9,6 +9,7 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' @@ -31,6 +32,7 @@ export function AccountSettingsScreen({}: Props) { const {currentAccount} = useSession() const {openModal} = useModalControls() const verifyEmailControl = useDialogControl() + const changeEmailControl = useDialogControl() const birthdayControl = useDialogControl() const changeHandleControl = useDialogControl() const exportCarControl = useDialogControl() @@ -95,7 +97,7 @@ export function AccountSettingsScreen({}: Props) { )} <SettingsList.PressableItem label={_(msg`Change email`)} - onPress={() => openModal({name: 'change-email'})}> + onPress={() => changeEmailControl.open()}> <SettingsList.ItemIcon icon={PencilIcon} /> <SettingsList.ItemText> <Trans>Change email</Trans> @@ -165,7 +167,14 @@ export function AccountSettingsScreen({}: Props) { </SettingsList.Container> </Layout.Content> - <VerifyEmailDialog control={verifyEmailControl} /> + <ChangeEmailDialog + control={changeEmailControl} + verifyEmailControl={verifyEmailControl} + /> + <VerifyEmailDialog + control={verifyEmailControl} + changeEmailControl={changeEmailControl} + /> <BirthDateSettingsDialog control={birthdayControl} /> <ChangeHandleDialog control={changeHandleControl} /> <ExportCarDialog control={exportCarControl} /> diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index dade2bf1f..76eb48203 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -3,7 +3,7 @@ import {LayoutAnimation, Pressable, View} from 'react-native' import {Linking} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' +import {msg, t, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' @@ -18,6 +18,7 @@ import { import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' +import * as persisted from '#/state/persisted' import {clearStorage} from '#/state/persisted' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' @@ -359,6 +360,17 @@ function DevOptions() { Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) } + const onPressUnsnoozeReminder = () => { + const lastEmailConfirm = new Date() + // wind back 3 days + lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3) + persisted.write('reminders', { + ...persisted.get('reminders'), + lastEmailConfirm: lastEmailConfirm.toISOString(), + }) + Toast.show(t`You probably want to restart the app now.`) + } + return ( <> <SettingsList.PressableItem @@ -397,6 +409,13 @@ function DevOptions() { </SettingsList.ItemText> </SettingsList.PressableItem> <SettingsList.PressableItem + onPress={onPressUnsnoozeReminder} + label={_(msg`Unsnooze email reminder`)}> + <SettingsList.ItemText> + <Trans>Unsnooze email reminder</Trans> + </SettingsList.ItemText> + </SettingsList.PressableItem> + <SettingsList.PressableItem onPress={() => clearAllStorage()} label={_(msg`Clear all storage data`)}> <SettingsList.ItemText> diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx index 0b327df79..3e341cd73 100644 --- a/src/screens/Settings/components/Email2FAToggle.tsx +++ b/src/screens/Settings/components/Email2FAToggle.tsx @@ -2,9 +2,10 @@ import React from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' import {useAgent, useSession} from '#/state/session' import {useDialogControl} from '#/components/Dialog' +import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import * as Prompt from '#/components/Prompt' import {DisableEmail2FADialog} from './DisableEmail2FADialog' import * as SettingsList from './SettingsList' @@ -12,9 +13,10 @@ import * as SettingsList from './SettingsList' export function Email2FAToggle() { const {_} = useLingui() const {currentAccount} = useSession() - const {openModal} = useModalControls() const disableDialogControl = useDialogControl() const enableDialogControl = useDialogControl() + const verifyEmailDialogControl = useDialogControl() + const changeEmailDialogControl = useDialogControl() const agent = useAgent() const enableEmailAuthFactor = React.useCallback(async () => { @@ -35,15 +37,17 @@ export function Email2FAToggle() { disableDialogControl.open() } else { if (!currentAccount.emailConfirmed) { - openModal({ - name: 'verify-email', - onSuccess: enableDialogControl.open, - }) + verifyEmailDialogControl.open() return } enableDialogControl.open() } - }, [currentAccount, enableDialogControl, openModal, disableDialogControl]) + }, [ + currentAccount, + enableDialogControl, + verifyEmailDialogControl, + disableDialogControl, + ]) return ( <> @@ -55,6 +59,18 @@ export function Email2FAToggle() { onConfirm={enableEmailAuthFactor} confirmButtonCta={_(msg`Enable`)} /> + <VerifyEmailDialog + control={verifyEmailDialogControl} + changeEmailControl={changeEmailDialogControl} + onCloseAfterVerifying={enableDialogControl.open} + reasonText={_( + msg`You need to verify your email address before you can enable email 2FA.`, + )} + /> + <ChangeEmailDialog + control={changeEmailDialogControl} + verifyEmailControl={verifyEmailDialogControl} + /> <SettingsList.BadgeButton label={ currentAccount?.emailAuthFactor ? _(msg`Change`) : _(msg`Enable`) diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 1709f0288..45c4fb467 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -55,16 +55,6 @@ export interface PostLanguagesSettingsModal { name: 'post-languages-settings' } -export interface VerifyEmailModal { - name: 'verify-email' - showReminder?: boolean - onSuccess?: () => void -} - -export interface ChangeEmailModal { - name: 'change-email' -} - export interface ChangePasswordModal { name: 'change-password' } @@ -84,8 +74,6 @@ export interface InAppBrowserConsentModal { export type Modal = // Account | DeleteAccountModal - | VerifyEmailModal - | ChangeEmailModal | ChangePasswordModal // Temp diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx deleted file mode 100644 index 003d3630e..000000000 --- a/src/view/com/modals/ChangeEmail.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import {useState} from 'react' -import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {cleanError} from '#/lib/strings/errors' -import {colors, s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {ScrollView, TextInput} from './util' - -enum Stages { - InputEmail, - ConfirmCode, - Done, -} - -export const snapPoints = ['90%'] - -export function Component() { - const pal = usePalette('default') - const {currentAccount} = useSession() - const agent = useAgent() - const {_} = useLingui() - const [stage, setStage] = useState<Stages>(Stages.InputEmail) - const [email, setEmail] = useState<string>(currentAccount?.email || '') - const [confirmationCode, setConfirmationCode] = useState<string>('') - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [error, setError] = useState<string>('') - const {isMobile} = useWebMediaQueries() - const {openModal, closeModal} = useModalControls() - - const onRequestChange = async () => { - if (email === currentAccount?.email) { - setError(_(msg`Enter your new email above`)) - return - } - setError('') - setIsProcessing(true) - try { - const res = await agent.com.atproto.server.requestEmailUpdate() - if (res.data.tokenRequired) { - setStage(Stages.ConfirmCode) - } else { - await agent.com.atproto.server.updateEmail({email: email.trim()}) - await agent.resumeSession(agent.session!) - Toast.show(_(msg({message: 'Email updated', context: 'toast'}))) - setStage(Stages.Done) - } - } catch (e) { - let err = cleanError(String(e)) - // TEMP - // while rollout is occuring, we're giving a temporary error message - // you can remove this any time after Oct2023 - // -prf - if (err === 'email must be confirmed (temporary)') { - err = _( - 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.`, - ) - } - setError(err) - } finally { - setIsProcessing(false) - } - } - - const onConfirm = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.updateEmail({ - email: email.trim(), - token: confirmationCode.trim(), - }) - await agent.resumeSession(agent.session!) - Toast.show(_(msg({message: 'Email updated', context: 'toast'}))) - setStage(Stages.Done) - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onVerify = async () => { - closeModal() - openModal({name: 'verify-email'}) - } - - return ( - <SafeAreaView style={[pal.view, s.flex1]}> - <ScrollView - testID="changeEmailModal" - style={[s.flex1, isMobile && {paddingHorizontal: 18}]}> - <View style={styles.titleSection}> - <Text type="title-lg" style={[pal.text, styles.title]}> - {stage === Stages.InputEmail ? _(msg`Change Your Email`) : ''} - {stage === Stages.ConfirmCode ? _(msg`Security Step Required`) : ''} - {stage === Stages.Done ? _(msg`Email Updated`) : ''} - </Text> - </View> - - <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> - {stage === Stages.InputEmail ? ( - <Trans>Enter your new email address below.</Trans> - ) : stage === Stages.ConfirmCode ? ( - <Trans> - An email has been sent to your previous address,{' '} - {currentAccount?.email || '(no email)'}. It includes a - confirmation code which you can enter below. - </Trans> - ) : ( - <Trans> - Your email has been updated but not verified. As a next step, - please verify your new email. - </Trans> - )} - </Text> - - {stage === Stages.InputEmail && ( - <TextInput - testID="emailInput" - style={[styles.textInput, pal.border, pal.text]} - placeholder="alice@mail.com" - placeholderTextColor={pal.colors.textLight} - value={email} - onChangeText={setEmail} - accessible={true} - accessibilityLabel={_(msg`Email`)} - accessibilityHint="" - autoCapitalize="none" - autoComplete="email" - autoCorrect={false} - /> - )} - {stage === Stages.ConfirmCode && ( - <TextInput - testID="confirmCodeInput" - style={[styles.textInput, pal.border, pal.text]} - placeholder="XXXXX-XXXXX" - placeholderTextColor={pal.colors.textLight} - value={confirmationCode} - onChangeText={setConfirmationCode} - accessible={true} - accessibilityLabel={_(msg`Confirmation code`)} - accessibilityHint="" - autoCapitalize="none" - autoComplete="off" - autoCorrect={false} - /> - )} - - {error ? ( - <ErrorMessage message={error} style={styles.error} /> - ) : undefined} - - <View style={[styles.btnContainer]}> - {isProcessing ? ( - <View style={styles.btn}> - <ActivityIndicator color="#fff" /> - </View> - ) : ( - <View style={{gap: 6}}> - {stage === Stages.InputEmail && ( - <Button - testID="requestChangeBtn" - type="primary" - onPress={onRequestChange} - accessibilityLabel={_(msg`Request Change`)} - accessibilityHint="" - label={_(msg`Request Change`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - {stage === Stages.ConfirmCode && ( - <Button - testID="confirmBtn" - type="primary" - onPress={onConfirm} - accessibilityLabel={_(msg`Confirm Change`)} - accessibilityHint="" - label={_(msg`Confirm Change`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - {stage === Stages.Done && ( - <Button - testID="verifyBtn" - type="primary" - onPress={onVerify} - accessibilityLabel={_(msg`Verify New Email`)} - accessibilityHint="" - label={_(msg`Verify New Email`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - <Button - testID="cancelBtn" - type="default" - onPress={() => { - closeModal() - }} - accessibilityLabel={_(msg`Cancel`)} - accessibilityHint="" - label={_(msg`Cancel`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - </View> - )} - </View> - </ScrollView> - </SafeAreaView> - ) -} - -const styles = StyleSheet.create({ - titleSection: { - paddingTop: isWeb ? 0 : 4, - paddingBottom: isWeb ? 14 : 10, - }, - title: { - textAlign: 'center', - fontWeight: '600', - marginBottom: 5, - }, - error: { - borderRadius: 6, - marginTop: 10, - }, - emailContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 14, - paddingVertical: 12, - }, - textInput: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 14, - paddingVertical: 10, - fontSize: 16, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnContainer: { - paddingTop: 20, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index b4572172c..d0b50c857 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -7,7 +7,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useModalControls, useModals} from '#/state/modals' import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' -import * as ChangeEmailModal from './ChangeEmail' import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' import * as DeleteAccountModal from './DeleteAccount' @@ -18,7 +17,6 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as LinkWarningModal from './LinkWarning' import * as UserAddRemoveListsModal from './UserAddRemoveLists' -import * as VerifyEmailModal from './VerifyEmail' const DEFAULT_SNAPPOINTS = ['90%'] const HANDLE_HEIGHT = 24 @@ -72,12 +70,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'post-languages-settings') { snapPoints = PostLanguagesSettingsModal.snapPoints element = <PostLanguagesSettingsModal.Component /> - } else if (activeModal?.name === 'verify-email') { - snapPoints = VerifyEmailModal.snapPoints - element = <VerifyEmailModal.Component {...activeModal} /> - } else if (activeModal?.name === 'change-email') { - snapPoints = ChangeEmailModal.snapPoints - element = <ChangeEmailModal.Component /> } else if (activeModal?.name === 'change-password') { snapPoints = ChangePasswordModal.snapPoints element = <ChangePasswordModal.Component /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 74ee7c210..524780099 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -6,7 +6,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' -import * as ChangeEmailModal from './ChangeEmail' import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' import * as CropImageModal from './CropImage.web' @@ -17,7 +16,6 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as LinkWarningModal from './LinkWarning' import * as UserAddRemoveLists from './UserAddRemoveLists' -import * as VerifyEmailModal from './VerifyEmail' export function ModalsContainer() { const {isModalActive, activeModals} = useModals() @@ -74,10 +72,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <ContentLanguagesSettingsModal.Component /> } else if (modal.name === 'post-languages-settings') { element = <PostLanguagesSettingsModal.Component /> - } else if (modal.name === 'verify-email') { - element = <VerifyEmailModal.Component {...modal} /> - } else if (modal.name === 'change-email') { - element = <ChangeEmailModal.Component /> } else if (modal.name === 'change-password') { element = <ChangePasswordModal.Component /> } else if (modal.name === 'link-warning') { diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx deleted file mode 100644 index 45444843a..000000000 --- a/src/view/com/modals/VerifyEmail.tsx +++ /dev/null @@ -1,342 +0,0 @@ -import React, {useState} from 'react' -import { - ActivityIndicator, - Pressable, - SafeAreaView, - StyleSheet, - View, -} from 'react-native' -import {Circle, Path, Svg} from 'react-native-svg' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {cleanError} from '#/lib/strings/errors' -import {colors, s} from '#/lib/styles' -import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {ScrollView, TextInput} from './util' - -export const snapPoints = ['90%'] - -enum Stages { - Reminder, - Email, - ConfirmCode, -} - -export function Component({ - showReminder, - onSuccess, -}: { - showReminder?: boolean - onSuccess?: () => void -}) { - const pal = usePalette('default') - const agent = useAgent() - const {currentAccount} = useSession() - const {_} = useLingui() - const [stage, setStage] = useState<Stages>( - showReminder ? Stages.Reminder : Stages.Email, - ) - const [confirmationCode, setConfirmationCode] = useState<string>('') - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [error, setError] = useState<string>('') - const {isMobile} = useWebMediaQueries() - const {openModal, closeModal} = useModalControls() - - React.useEffect(() => { - if (!currentAccount) { - logger.error(`VerifyEmail modal opened without currentAccount`) - closeModal() - } - }, [currentAccount, closeModal]) - - const onSendEmail = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.requestEmailConfirmation() - setStage(Stages.ConfirmCode) - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onConfirm = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.confirmEmail({ - email: (currentAccount?.email || '').trim(), - token: confirmationCode.trim(), - }) - await agent.resumeSession(agent.session!) - Toast.show(_(msg({message: 'Email verified', context: 'toast'}))) - closeModal() - onSuccess?.() - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onEmailIncorrect = () => { - closeModal() - openModal({name: 'change-email'}) - } - - return ( - <SafeAreaView style={[pal.view, s.flex1]}> - <ScrollView - testID="verifyEmailModal" - style={[s.flex1, isMobile && {paddingHorizontal: 18}]}> - {stage === Stages.Reminder && <ReminderIllustration />} - <View style={styles.titleSection}> - <Text type="title-lg" style={[pal.text, styles.title]}> - {stage === Stages.Reminder ? ( - <Trans>Please Verify Your Email</Trans> - ) : stage === Stages.Email ? ( - <Trans>Verify Your Email</Trans> - ) : stage === Stages.ConfirmCode ? ( - <Trans>Enter Confirmation Code</Trans> - ) : ( - '' - )} - </Text> - </View> - - <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> - {stage === Stages.Reminder ? ( - <Trans> - Your email has not yet been verified. This is an important - security step which we recommend. - </Trans> - ) : stage === Stages.Email ? ( - <Trans> - This is important in case you ever need to change your email or - reset your password. - </Trans> - ) : stage === Stages.ConfirmCode ? ( - <Trans> - An email has been sent to {currentAccount?.email || '(no email)'}. - It includes a confirmation code which you can enter below. - </Trans> - ) : ( - '' - )} - </Text> - - {stage === Stages.Email ? ( - <> - <View style={styles.emailContainer}> - <FontAwesomeIcon - icon="envelope" - color={pal.colors.text} - size={16} - /> - <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}> - {currentAccount?.email || _(msg`(no email)`)} - </Text> - </View> - <Pressable - accessibilityRole="link" - accessibilityLabel={_(msg`Change my email`)} - accessibilityHint="" - onPress={onEmailIncorrect} - style={styles.changeEmailLink}> - <Text type="lg" style={pal.link}> - <Trans>Change</Trans> - </Text> - </Pressable> - </> - ) : stage === Stages.ConfirmCode ? ( - <TextInput - testID="confirmCodeInput" - style={[styles.textInput, pal.border, pal.text]} - placeholder="XXXXX-XXXXX" - placeholderTextColor={pal.colors.textLight} - value={confirmationCode} - onChangeText={setConfirmationCode} - accessible={true} - accessibilityLabel={_(msg`Confirmation code`)} - accessibilityHint="" - autoCapitalize="none" - autoComplete="one-time-code" - autoCorrect={false} - /> - ) : undefined} - - {error ? ( - <ErrorMessage message={error} style={styles.error} /> - ) : undefined} - - <View style={[styles.btnContainer]}> - {isProcessing ? ( - <View style={styles.btn}> - <ActivityIndicator color="#fff" /> - </View> - ) : ( - <View style={{gap: 6}}> - {stage === Stages.Reminder && ( - <Button - testID="getStartedBtn" - type="primary" - onPress={() => setStage(Stages.Email)} - accessibilityLabel={_(msg`Get Started`)} - accessibilityHint="" - label={_(msg`Get Started`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - {stage === Stages.Email && ( - <> - <Button - testID="sendEmailBtn" - type="primary" - onPress={onSendEmail} - accessibilityLabel={_(msg`Send Confirmation Email`)} - accessibilityHint="" - label={_(msg`Send Confirmation Email`)} - labelContainerStyle={{ - justifyContent: 'center', - padding: 4, - }} - labelStyle={[s.f18]} - /> - <Button - testID="haveCodeBtn" - type="default" - accessibilityLabel={_(msg`I have a code`)} - accessibilityHint="" - label={_(msg`I have a confirmation code`)} - labelContainerStyle={{ - justifyContent: 'center', - padding: 4, - }} - labelStyle={[s.f18]} - onPress={() => setStage(Stages.ConfirmCode)} - /> - </> - )} - {stage === Stages.ConfirmCode && ( - <Button - testID="confirmBtn" - type="primary" - onPress={onConfirm} - accessibilityLabel={_(msg`Confirm`)} - accessibilityHint="" - label={_(msg`Confirm`)} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - )} - <Button - testID="cancelBtn" - type="default" - onPress={() => { - closeModal() - }} - accessibilityLabel={ - stage === Stages.Reminder - ? _(msg`Not right now`) - : _(msg`Cancel`) - } - accessibilityHint="" - label={ - stage === Stages.Reminder - ? _(msg`Not right now`) - : _(msg`Cancel`) - } - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - </View> - )} - </View> - </ScrollView> - </SafeAreaView> - ) -} - -function ReminderIllustration() { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - return ( - <View style={[pal.viewLight, {borderRadius: 8, marginBottom: 20}]}> - <Svg viewBox="0 0 112 84" fill="none" height={200}> - <Path - fillRule="evenodd" - clipRule="evenodd" - 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" - fill={palInverted.colors.background} - /> - <Path - fillRule="evenodd" - clipRule="evenodd" - 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" - fill={palInverted.colors.background} - /> - <Circle cx="82" cy="61" r="13" fill="#20BC07" /> - <Path d="M75 61L80 66L89 57" stroke="white" strokeWidth="2" /> - </Svg> - </View> - ) -} - -const styles = StyleSheet.create({ - titleSection: { - paddingTop: isWeb ? 0 : 4, - paddingBottom: isWeb ? 14 : 10, - }, - title: { - textAlign: 'center', - fontWeight: '600', - marginBottom: 5, - }, - error: { - borderRadius: 6, - marginTop: 10, - }, - emailContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingHorizontal: 14, - marginTop: 10, - }, - changeEmailLink: { - marginHorizontal: 12, - marginBottom: 12, - }, - textInput: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 14, - paddingVertical: 10, - fontSize: 16, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnContainer: { - paddingTop: 20, - }, -}) |