diff options
Diffstat (limited to 'src/view/com/modals')
-rw-r--r-- | src/view/com/modals/ChangeEmail.tsx | 280 | ||||
-rw-r--r-- | src/view/com/modals/Confirm.tsx | 3 | ||||
-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 | 296 |
5 files changed, 592 insertions, 1 deletions
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx new file mode 100644 index 000000000..c92dabdca --- /dev/null +++ b/src/view/com/modals/ChangeEmail.tsx @@ -0,0 +1,280 @@ +import React, {useState} from 'react' +import { + ActivityIndicator, + KeyboardAvoidingView, + SafeAreaView, + StyleSheet, + View, +} from 'react-native' +import {ScrollView, TextInput} from './util' +import {observer} from 'mobx-react-lite' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {ErrorMessage} from '../util/error/ErrorMessage' +import * as Toast from '../util/Toast' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {isWeb} from 'platform/detection' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {cleanError} from 'lib/strings/errors' + +enum Stages { + InputEmail, + ConfirmCode, + Done, +} + +export const snapPoints = ['90%'] + +export const Component = observer(function Component({}: {}) { + const pal = usePalette('default') + const store = useStores() + const [stage, setStage] = useState<Stages>(Stages.InputEmail) + const [email, setEmail] = useState<string>( + store.session.currentSession?.email || '', + ) + const [confirmationCode, setConfirmationCode] = useState<string>('') + const [isProcessing, setIsProcessing] = useState<boolean>(false) + const [error, setError] = useState<string>('') + const {isMobile} = useWebMediaQueries() + + const onRequestChange = async () => { + if (email === store.session.currentSession?.email) { + setError('Enter your new email above') + return + } + setError('') + setIsProcessing(true) + try { + const res = await store.agent.com.atproto.server.requestEmailUpdate() + if (res.data.tokenRequired) { + setStage(Stages.ConfirmCode) + } else { + await store.agent.com.atproto.server.updateEmail({email: email.trim()}) + store.session.updateLocalAccountData({ + email: email.trim(), + emailConfirmed: false, + }) + Toast.show('Email updated') + 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 = `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 store.agent.com.atproto.server.updateEmail({ + email: email.trim(), + token: confirmationCode.trim(), + }) + store.session.updateLocalAccountData({ + email: email.trim(), + emailConfirmed: false, + }) + Toast.show('Email updated') + setStage(Stages.Done) + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onVerify = async () => { + store.shell.closeModal() + store.shell.openModal({name: 'verify-email'}) + } + + return ( + <KeyboardAvoidingView + behavior="padding" + style={[pal.view, styles.container]}> + <SafeAreaView style={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 ? 'Change Your Email' : ''} + {stage === Stages.ConfirmCode ? 'Security Step Required' : ''} + {stage === Stages.Done ? 'Email Updated' : ''} + </Text> + </View> + + <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> + {stage === Stages.InputEmail ? ( + <>Enter your new email address below.</> + ) : stage === Stages.ConfirmCode ? ( + <> + An email has been sent to your previous address,{' '} + {store.session.currentSession?.email || ''}. It includes a + confirmation code which you can enter below. + </> + ) : ( + <> + Your email has been updated but not verified. As a next step, + please verify your new email. + </> + )} + </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="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="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="Request Change" + accessibilityHint="" + label="Request Change" + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + )} + {stage === Stages.ConfirmCode && ( + <Button + testID="confirmBtn" + type="primary" + onPress={onConfirm} + accessibilityLabel="Confirm Change" + accessibilityHint="" + label="Confirm Change" + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + )} + {stage === Stages.Done && ( + <Button + testID="verifyBtn" + type="primary" + onPress={onVerify} + accessibilityLabel="Verify New Email" + accessibilityHint="" + label="Verify New Email" + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + )} + <Button + testID="cancelBtn" + type="default" + onPress={() => store.shell.closeModal()} + accessibilityLabel="Cancel" + accessibilityHint="" + label="Cancel" + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + </View> + )} + </View> + </ScrollView> + </SafeAreaView> + </KeyboardAvoidingView> + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isWeb ? 0 : 40, + }, + 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/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 270177182..c1324b1cb 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -23,6 +23,7 @@ export function Component({ onPressCancel, confirmBtnText, confirmBtnStyle, + cancelBtnText, }: ConfirmModal) { const pal = usePalette('default') const store = useStores() @@ -84,7 +85,7 @@ export function Component({ accessibilityLabel="Cancel" accessibilityHint=""> <Text type="button-lg" style={pal.textLight}> - Cancel + {cancelBtnText ?? 'Cancel'} </Text> </TouchableOpacity> )} diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index d79c77db3..8590a2698 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -30,6 +30,8 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as ModerationDetailsModal from './ModerationDetails' import * as BirthDateSettingsModal from './BirthDateSettings' +import * as VerifyEmailModal from './VerifyEmail' +import * as ChangeEmailModal from './ChangeEmail' const DEFAULT_SNAPPOINTS = ['90%'] @@ -136,6 +138,12 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'birth-date-settings') { snapPoints = BirthDateSettingsModal.snapPoints element = <BirthDateSettingsModal.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 { return null } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 3e87e0e3c..7548fb806 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -28,6 +28,8 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as ModerationDetailsModal from './ModerationDetails' import * as BirthDateSettingsModal from './BirthDateSettings' +import * as VerifyEmailModal from './VerifyEmail' +import * as ChangeEmailModal from './ChangeEmail' export const ModalsContainer = observer(function ModalsContainer() { const store = useStores() @@ -110,6 +112,10 @@ function Modal({modal}: {modal: ModalIface}) { element = <ModerationDetailsModal.Component {...modal} /> } else if (modal.name === 'birth-date-settings') { element = <BirthDateSettingsModal.Component /> + } else if (modal.name === 'verify-email') { + element = <VerifyEmailModal.Component {...modal} /> + } else if (modal.name === 'change-email') { + element = <ChangeEmailModal.Component /> } else { return null } diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx new file mode 100644 index 000000000..1b4ddcda4 --- /dev/null +++ b/src/view/com/modals/VerifyEmail.tsx @@ -0,0 +1,296 @@ +import React, {useState} from 'react' +import { + ActivityIndicator, + KeyboardAvoidingView, + Pressable, + SafeAreaView, + StyleSheet, + View, +} from 'react-native' +import {ScrollView, TextInput} from './util' +import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {ErrorMessage} from '../util/error/ErrorMessage' +import * as Toast from '../util/Toast' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {isWeb} from 'platform/detection' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {cleanError} from 'lib/strings/errors' + +export const snapPoints = ['90%'] + +enum Stages { + Reminder, + Email, + ConfirmCode, +} + +export const Component = observer(function Component({ + showReminder, +}: { + showReminder?: boolean +}) { + const pal = usePalette('default') + const store = useStores() + 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 onSendEmail = async () => { + setError('') + setIsProcessing(true) + try { + await store.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 store.agent.com.atproto.server.confirmEmail({ + email: (store.session.currentSession?.email || '').trim(), + token: confirmationCode.trim(), + }) + store.session.updateLocalAccountData({emailConfirmed: true}) + Toast.show('Email verified') + store.shell.closeModal() + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onEmailIncorrect = () => { + store.shell.closeModal() + store.shell.openModal({name: 'change-email'}) + } + + return ( + <KeyboardAvoidingView + behavior="padding" + style={[pal.view, styles.container]}> + <SafeAreaView style={s.flex1}> + <ScrollView + testID="verifyEmailModal" + style={[s.flex1, isMobile && {paddingHorizontal: 18}]}> + <View style={styles.titleSection}> + <Text type="title-lg" style={[pal.text, styles.title]}> + {stage === Stages.Reminder ? 'Please Verify Your Email' : ''} + {stage === Stages.ConfirmCode ? 'Enter Confirmation Code' : ''} + {stage === Stages.Email ? 'Verify Your Email' : ''} + </Text> + </View> + + <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> + {stage === Stages.Reminder ? ( + <> + Your email has not yet been verified. This is an important + security step which we recommend. + </> + ) : stage === Stages.Email ? ( + <> + This is important in case you ever need to change your email or + reset your password. + </> + ) : stage === Stages.ConfirmCode ? ( + <> + An email has been sent to{' '} + {store.session.currentSession?.email || ''}. It includes a + confirmation code which you can enter below. + </> + ) : ( + '' + )} + </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}]}> + {store.session.currentSession?.email || ''} + </Text> + </View> + <Pressable + accessibilityRole="link" + accessibilityLabel="Change my email" + accessibilityHint="" + onPress={onEmailIncorrect} + style={styles.changeEmailLink}> + <Text type="lg" style={pal.link}> + Change + </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="Confirmation code" + accessibilityHint="" + autoCapitalize="none" + autoComplete="off" + 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="Get Started" + accessibilityHint="" + label="Get Started" + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + )} + {stage === Stages.Email && ( + <> + <Button + testID="sendEmailBtn" + type="primary" + onPress={onSendEmail} + accessibilityLabel="Send Confirmation Email" + accessibilityHint="" + label="Send Confirmation Email" + labelContainerStyle={{ + justifyContent: 'center', + padding: 4, + }} + labelStyle={[s.f18]} + /> + <Button + testID="haveCodeBtn" + type="default" + accessibilityLabel="I have a code" + accessibilityHint="" + label="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="Confirm" + accessibilityHint="" + label="Confirm" + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + )} + <Button + testID="cancelBtn" + type="default" + onPress={() => store.shell.closeModal()} + accessibilityLabel={ + stage === Stages.Reminder ? 'Not right now' : 'Cancel' + } + accessibilityHint="" + label={stage === Stages.Reminder ? 'Not right now' : 'Cancel'} + labelContainerStyle={{justifyContent: 'center', padding: 4}} + labelStyle={[s.f18]} + /> + </View> + )} + </View> + </ScrollView> + </SafeAreaView> + </KeyboardAvoidingView> + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isWeb ? 0 : 40, + }, + 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, + }, +}) |