diff options
Diffstat (limited to 'src/components/dialogs')
-rw-r--r-- | src/components/dialogs/ChangeEmailDialog.tsx | 259 | ||||
-rw-r--r-- | src/components/dialogs/VerifyEmailDialog.tsx | 159 |
2 files changed, 370 insertions, 48 deletions
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" |