diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/auth/LoggedOut.tsx | 18 | ||||
-rw-r--r-- | src/view/com/auth/create/CaptchaWebView.tsx | 14 | ||||
-rw-r--r-- | src/view/com/auth/create/CreateAccount.tsx | 230 | ||||
-rw-r--r-- | src/view/com/auth/create/Policies.tsx | 14 | ||||
-rw-r--r-- | src/view/com/auth/create/Step1.tsx | 261 | ||||
-rw-r--r-- | src/view/com/auth/create/Step2.tsx | 140 | ||||
-rw-r--r-- | src/view/com/auth/create/Step3.tsx | 114 | ||||
-rw-r--r-- | src/view/com/auth/create/StepHeader.tsx | 44 | ||||
-rw-r--r-- | src/view/com/auth/login/ChooseAccountForm.tsx | 162 | ||||
-rw-r--r-- | src/view/com/auth/login/ForgotPasswordForm.tsx | 228 | ||||
-rw-r--r-- | src/view/com/auth/login/Login.tsx | 164 | ||||
-rw-r--r-- | src/view/com/auth/login/LoginForm.tsx | 298 | ||||
-rw-r--r-- | src/view/com/auth/login/PasswordUpdatedForm.tsx | 48 | ||||
-rw-r--r-- | src/view/com/auth/login/SetNewPasswordForm.tsx | 211 | ||||
-rw-r--r-- | src/view/com/auth/login/styles.ts | 118 | ||||
-rw-r--r-- | src/view/com/auth/server-input/index.tsx | 2 |
16 files changed, 28 insertions, 2038 deletions
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 603abbab2..b22bbb7fe 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -5,16 +5,16 @@ import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' import {useNavigation} from '@react-navigation/native' -import {isIOS, isNative} from 'platform/detection' -import {Login} from 'view/com/auth/login/Login' -import {CreateAccount} from 'view/com/auth/create/CreateAccount' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' +import {isIOS, isNative} from '#/platform/detection' +import {Login} from '#/screens/Login' +import {Signup} from '#/screens/Signup' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' +import {s} from '#/lib/styles' +import {usePalette} from '#/lib/hooks/usePalette' +import {useAnalytics} from '#/lib/analytics/analytics' import {SplashScreen} from './SplashScreen' import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import { useLoggedOutView, useLoggedOutViewControls, @@ -148,7 +148,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { /> ) : undefined} {screenState === ScreenState.S_CreateAccount ? ( - <CreateAccount + <Signup onPressBack={() => setScreenState(ScreenState.S_LoginOrCreateAccount) } diff --git a/src/view/com/auth/create/CaptchaWebView.tsx b/src/view/com/auth/create/CaptchaWebView.tsx index b0de8b4a4..06b605e4d 100644 --- a/src/view/com/auth/create/CaptchaWebView.tsx +++ b/src/view/com/auth/create/CaptchaWebView.tsx @@ -2,7 +2,7 @@ import React from 'react' import {WebView, WebViewNavigation} from 'react-native-webview' import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' import {StyleSheet} from 'react-native' -import {CreateAccountState} from 'view/com/auth/create/state' +import {SignupState} from '#/screens/Signup/state' const ALLOWED_HOSTS = [ 'bsky.social', @@ -17,24 +17,24 @@ const ALLOWED_HOSTS = [ export function CaptchaWebView({ url, stateParam, - uiState, + state, onSuccess, onError, }: { url: string stateParam: string - uiState?: CreateAccountState + state?: SignupState onSuccess: (code: string) => void onError: () => void }) { const redirectHost = React.useMemo(() => { - if (!uiState?.serviceUrl) return 'bsky.app' + if (!state?.serviceUrl) return 'bsky.app' - return uiState?.serviceUrl && - new URL(uiState?.serviceUrl).host === 'staging.bsky.dev' + return state?.serviceUrl && + new URL(state?.serviceUrl).host === 'staging.bsky.dev' ? 'staging.bsky.app' : 'bsky.app' - }, [uiState?.serviceUrl]) + }, [state?.serviceUrl]) const wasSuccessful = React.useRef(false) diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx deleted file mode 100644 index d193802fe..000000000 --- a/src/view/com/auth/create/CreateAccount.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - ScrollView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useCreateAccount, useSubmitCreateAccount} from './state' -import {useServiceQuery} from '#/state/queries/service' -import {FEEDBACK_FORM_URL, HITSLOP_10} from '#/lib/constants' - -import {Step1} from './Step1' -import {Step2} from './Step2' -import {Step3} from './Step3' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {TextLink} from '../../util/Link' -import {getAgent} from 'state/session' -import {createFullHandle, validateHandle} from 'lib/strings/handles' - -export function CreateAccount({onPressBack}: {onPressBack: () => void}) { - const {screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - const [uiState, uiDispatch] = useCreateAccount() - const {isTabletOrDesktop} = useWebMediaQueries() - const submit = useSubmitCreateAccount(uiState, uiDispatch) - - React.useEffect(() => { - screen('CreateAccount') - }, [screen]) - - // fetch service info - // = - - const { - data: serviceInfo, - isFetching: serviceInfoIsFetching, - error: serviceInfoError, - refetch: refetchServiceInfo, - } = useServiceQuery(uiState.serviceUrl) - - React.useEffect(() => { - if (serviceInfo) { - uiDispatch({type: 'set-service-description', value: serviceInfo}) - uiDispatch({type: 'set-error', value: ''}) - } else if (serviceInfoError) { - uiDispatch({ - type: 'set-error', - value: _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - }) - } - }, [_, uiDispatch, serviceInfo, serviceInfoError]) - - // event handlers - // = - - const onPressBackInner = React.useCallback(() => { - if (uiState.canBack) { - uiDispatch({type: 'back'}) - } else { - onPressBack() - } - }, [uiState, uiDispatch, onPressBack]) - - const onPressNext = React.useCallback(async () => { - if (!uiState.canNext) { - return - } - - if (uiState.step === 2) { - if (!validateHandle(uiState.handle, uiState.userDomain).overall) { - return - } - - uiDispatch({type: 'set-processing', value: true}) - try { - const res = await getAgent().resolveHandle({ - handle: createFullHandle(uiState.handle, uiState.userDomain), - }) - - if (res.data.did) { - uiDispatch({ - type: 'set-error', - value: _(msg`That handle is already taken.`), - }) - return - } - } catch (e) { - // Don't need to handle - } finally { - uiDispatch({type: 'set-processing', value: false}) - } - - if (!uiState.isCaptchaRequired) { - try { - await submit() - } catch { - // dont need to handle here - } - // We don't need to go to the next page if there wasn't a captcha required - return - } - } - - uiDispatch({type: 'next'}) - }, [ - uiState.canNext, - uiState.step, - uiState.isCaptchaRequired, - uiState.handle, - uiState.userDomain, - uiDispatch, - _, - submit, - ]) - - // rendering - // = - - return ( - <LoggedOutLayout - leadin="" - title={_(msg`Create Account`)} - description={_(msg`We're so excited to have you join us!`)}> - <ScrollView - testID="createAccount" - style={pal.view} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag"> - <View style={styles.stepContainer}> - {uiState.step === 1 && ( - <Step1 uiState={uiState} uiDispatch={uiDispatch} /> - )} - {uiState.step === 2 && ( - <Step2 uiState={uiState} uiDispatch={uiDispatch} /> - )} - {uiState.step === 3 && ( - <Step3 uiState={uiState} uiDispatch={uiDispatch} /> - )} - </View> - <View style={[s.flexRow, s.pl20, s.pr20]}> - <TouchableOpacity - onPress={onPressBackInner} - testID="backBtn" - accessibilityRole="button" - hitSlop={HITSLOP_10}> - <Text type="xl" style={pal.link}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {uiState.canNext ? ( - <TouchableOpacity - testID="nextBtn" - onPress={onPressNext} - accessibilityRole="button" - hitSlop={HITSLOP_10}> - {uiState.isProcessing ? ( - <ActivityIndicator /> - ) : ( - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - )} - </TouchableOpacity> - ) : serviceInfoError ? ( - <TouchableOpacity - testID="retryConnectBtn" - onPress={() => refetchServiceInfo()} - accessibilityRole="button" - accessibilityLabel={_(msg`Retry`)} - accessibilityHint="" - accessibilityLiveRegion="polite" - hitSlop={HITSLOP_10}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Retry</Trans> - </Text> - </TouchableOpacity> - ) : serviceInfoIsFetching ? ( - <> - <ActivityIndicator color="#fff" /> - <Text type="xl" style={[pal.text, s.pr5]}> - <Trans>Connecting...</Trans> - </Text> - </> - ) : undefined} - </View> - - <View style={styles.stepContainer}> - <View - style={[ - s.flexRow, - s.alignCenter, - pal.viewLight, - {borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12}, - ]}> - <Text type="md" style={pal.textLight}> - <Trans>Having trouble?</Trans>{' '} - </Text> - <TextLink - type="md" - style={pal.link} - text={_(msg`Contact support`)} - href={FEEDBACK_FORM_URL({email: uiState.email})} - /> - </View> - </View> - - <View style={{height: isTabletOrDesktop ? 50 : 400}} /> - </ScrollView> - </LoggedOutLayout> - ) -} - -const styles = StyleSheet.create({ - stepContainer: { - paddingHorizontal: 20, - paddingVertical: 20, - }, -}) diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx index 803e2ad32..f69b4bdbd 100644 --- a/src/view/com/auth/create/Policies.tsx +++ b/src/view/com/auth/create/Policies.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' +import {Linking, StyleSheet, View} from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -17,9 +17,11 @@ type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema export const Policies = ({ serviceDescription, needsGuardian, + under13, }: { serviceDescription: ServiceDescription needsGuardian: boolean + under13: boolean }) => { const pal = usePalette('default') const {_} = useLingui() @@ -58,6 +60,7 @@ export const Policies = ({ href={tos} text={_(msg`Terms of Service`)} style={[pal.link, s.underline]} + onPress={() => Linking.openURL(tos)} />, ) } @@ -68,6 +71,7 @@ export const Policies = ({ href={pp} text={_(msg`Privacy Policy`)} style={[pal.link, s.underline]} + onPress={() => Linking.openURL(pp)} />, ) } @@ -86,14 +90,18 @@ export const Policies = ({ <Text style={pal.textLight}> <Trans>By creating an account you agree to the {els}.</Trans> </Text> - {needsGuardian && ( + {under13 ? ( + <Text style={[pal.textLight, s.bold]}> + You must be 13 years of age or older to sign up. + </Text> + ) : needsGuardian ? ( <Text style={[pal.textLight, s.bold]}> <Trans> If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf. </Trans> </Text> - )} + ) : undefined} </View> ) } diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx deleted file mode 100644 index 1f6852f8c..000000000 --- a/src/view/com/auth/create/Step1.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - Keyboard, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {CreateAccountState, CreateAccountDispatch, is18} from './state' -import {Text} from 'view/com/util/text/Text' -import {DateInput} from 'view/com/util/forms/DateInput' -import {StepHeader} from './StepHeader' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {TextInput} from '../util/TextInput' -import {Policies} from './Policies' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {isWeb} from 'platform/detection' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {logger} from '#/logger' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {useDialogControl} from '#/components/Dialog' - -import {ServerInputDialog} from '../server-input' -import {toNiceDomain} from '#/lib/strings/url-helpers' - -function sanitizeDate(date: Date): Date { - if (!date || date.toString() === 'Invalid Date') { - logger.error(`Create account: handled invalid date for birthDate`, { - hasDate: !!date, - }) - return new Date() - } - return date -} - -export function Step1({ - uiState, - uiDispatch, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch -}) { - const pal = usePalette('default') - const {_} = useLingui() - const serverInputControl = useDialogControl() - - const onPressSelectService = React.useCallback(() => { - serverInputControl.open() - Keyboard.dismiss() - }, [serverInputControl]) - - const birthDate = React.useMemo(() => { - return sanitizeDate(uiState.birthDate) - }, [uiState.birthDate]) - - return ( - <View> - <ServerInputDialog - control={serverInputControl} - onSelect={url => uiDispatch({type: 'set-service-url', value: url})} - /> - <StepHeader uiState={uiState} title={_(msg`Your account`)} /> - - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : undefined} - - <View style={s.pb20}> - <Text type="md-medium" style={[pal.text, s.mb2]}> - <Trans>Hosting provider</Trans> - </Text> - <View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}> - <View - style={[ - pal.borderDark, - {flexDirection: 'row', alignItems: 'center'}, - ]}> - <FontAwesomeIcon - icon="globe" - style={[pal.textLight, {marginLeft: 14}]} - /> - <TouchableOpacity - testID="selectServiceButton" - style={{ - flexDirection: 'row', - flex: 1, - alignItems: 'center', - }} - onPress={onPressSelectService} - accessibilityRole="button" - accessibilityLabel={_(msg`Select service`)} - accessibilityHint={_(msg`Sets server for the Bluesky client`)}> - <Text - type="xl" - style={[ - pal.text, - { - flex: 1, - paddingVertical: 10, - paddingRight: 12, - paddingLeft: 10, - }, - ]}> - {toNiceDomain(uiState.serviceUrl)} - </Text> - <View - style={[ - pal.btn, - { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 6, - paddingVertical: 6, - paddingHorizontal: 8, - marginHorizontal: 6, - }, - ]}> - <FontAwesomeIcon - icon="pen" - size={12} - style={pal.textLight as FontAwesomeIconStyle} - /> - </View> - </TouchableOpacity> - </View> - </View> - </View> - - {!uiState.serviceDescription ? ( - <ActivityIndicator /> - ) : ( - <> - {uiState.isInviteCodeRequired && ( - <View style={s.pb20}> - <Text type="md-medium" style={[pal.text, s.mb2]}> - <Trans>Invite code</Trans> - </Text> - <TextInput - testID="inviteCodeInput" - icon="ticket" - placeholder={_(msg`Required for this provider`)} - value={uiState.inviteCode} - editable - onChange={value => uiDispatch({type: 'set-invite-code', value})} - accessibilityLabel={_(msg`Invite code`)} - accessibilityHint={_(msg`Input invite code to proceed`)} - autoCapitalize="none" - autoComplete="off" - autoCorrect={false} - autoFocus={true} - /> - </View> - )} - - {!uiState.isInviteCodeRequired || uiState.inviteCode ? ( - <> - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="email"> - <Trans>Email address</Trans> - </Text> - <TextInput - testID="emailInput" - icon="envelope" - placeholder={_(msg`Enter your email address`)} - value={uiState.email} - editable - onChange={value => uiDispatch({type: 'set-email', value})} - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_(msg`Input email for Bluesky account`)} - accessibilityLabelledBy="email" - autoCapitalize="none" - autoComplete="email" - autoCorrect={false} - autoFocus={!uiState.isInviteCodeRequired} - /> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="password"> - <Trans>Password</Trans> - </Text> - <TextInput - testID="passwordInput" - icon="lock" - placeholder={_(msg`Choose your password`)} - value={uiState.password} - editable - secureTextEntry - onChange={value => uiDispatch({type: 'set-password', value})} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={_(msg`Set password`)} - accessibilityLabelledBy="password" - autoCapitalize="none" - autoComplete="new-password" - autoCorrect={false} - /> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="birthDate"> - <Trans>Your birth date</Trans> - </Text> - <DateInput - handleAsUTC - testID="birthdayInput" - value={birthDate} - onChange={value => - uiDispatch({type: 'set-birth-date', value}) - } - buttonType="default-light" - buttonStyle={[pal.border, styles.dateInputButton]} - buttonLabelType="lg" - accessibilityLabel={_(msg`Birthday`)} - accessibilityHint={_(msg`Enter your birth date`)} - accessibilityLabelledBy="birthDate" - /> - </View> - - {uiState.serviceDescription && ( - <Policies - serviceDescription={uiState.serviceDescription} - needsGuardian={!is18(uiState)} - /> - )} - </> - ) : undefined} - </> - )} - </View> - ) -} - -const styles = StyleSheet.create({ - error: { - borderRadius: 6, - marginBottom: 10, - }, - dateInputButton: { - borderWidth: 1, - borderRadius: 6, - paddingVertical: 14, - }, - // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. - touchable: { - ...(isWeb && {cursor: 'pointer'}), - }, -}) diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx deleted file mode 100644 index 5c262977f..000000000 --- a/src/view/com/auth/create/Step2.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {CreateAccountState, CreateAccountDispatch} from './state' -import {Text} from 'view/com/util/text/Text' -import {StepHeader} from './StepHeader' -import {s} from 'lib/styles' -import {TextInput} from '../util/TextInput' -import { - createFullHandle, - IsValidHandle, - validateHandle, -} from 'lib/strings/handles' -import {usePalette} from 'lib/hooks/usePalette' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {atoms as a, useTheme} from '#/alf' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' -import {useFocusEffect} from '@react-navigation/native' - -/** STEP 3: Your user handle - * @field User handle - */ -export function Step2({ - uiState, - uiDispatch, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch -}) { - const pal = usePalette('default') - const {_} = useLingui() - const t = useTheme() - - const [validCheck, setValidCheck] = React.useState<IsValidHandle>({ - handleChars: false, - frontLength: false, - totalLength: true, - overall: false, - }) - - useFocusEffect( - React.useCallback(() => { - setValidCheck(validateHandle(uiState.handle, uiState.userDomain)) - - // Disabling this, because we only want to run this when we focus the screen - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []), - ) - - const onHandleChange = React.useCallback( - (value: string) => { - if (uiState.error) { - uiDispatch({type: 'set-error', value: ''}) - } - - setValidCheck(validateHandle(value, uiState.userDomain)) - uiDispatch({type: 'set-handle', value}) - }, - [uiDispatch, uiState.error, uiState.userDomain], - ) - - return ( - <View> - <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> - <View style={s.pb10}> - <View style={s.mb20}> - <TextInput - testID="handleInput" - icon="at" - placeholder="e.g. alice" - value={uiState.handle} - editable - autoFocus - autoComplete="off" - autoCorrect={false} - onChange={onHandleChange} - // TODO: Add explicit text label - accessibilityLabel={_(msg`User handle`)} - accessibilityHint={_(msg`Input your user handle`)} - /> - <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> - <Trans>Your full handle will be</Trans>{' '} - <Text type="lg-bold" style={pal.text}> - @{createFullHandle(uiState.handle, uiState.userDomain)} - </Text> - </Text> - </View> - <View - style={[ - a.w_full, - a.rounded_sm, - a.border, - a.p_md, - a.gap_sm, - t.atoms.border_contrast_low, - ]}> - {uiState.error ? ( - <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> - <IsValidIcon valid={false} /> - <Text style={[t.atoms.text, a.text_md, a.flex]}> - {uiState.error} - </Text> - </View> - ) : undefined} - <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> - <IsValidIcon valid={validCheck.handleChars} /> - <Text style={[t.atoms.text, a.text_md, a.flex]}> - <Trans>May only contain letters and numbers</Trans> - </Text> - </View> - <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> - <IsValidIcon - valid={validCheck.frontLength && validCheck.totalLength} - /> - {!validCheck.totalLength ? ( - <Text style={[t.atoms.text]}> - <Trans>May not be longer than 253 characters</Trans> - </Text> - ) : ( - <Text style={[t.atoms.text, a.text_md]}> - <Trans>Must be at least 3 characters</Trans> - </Text> - )} - </View> - </View> - </View> - </View> - ) -} - -function IsValidIcon({valid}: {valid: boolean}) { - const t = useTheme() - - if (!valid) { - return <Times size="md" style={{color: t.palette.negative_500}} /> - } - - return <Check size="md" style={{color: t.palette.positive_700}} /> -} diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx deleted file mode 100644 index 53fdfdde8..000000000 --- a/src/view/com/auth/create/Step3.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' -import { - CreateAccountState, - CreateAccountDispatch, - useSubmitCreateAccount, -} from './state' -import {StepHeader} from './StepHeader' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {isWeb} from 'platform/detection' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {nanoid} from 'nanoid/non-secure' -import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView' -import {useTheme} from 'lib/ThemeContext' -import {createFullHandle} from 'lib/strings/handles' - -const CAPTCHA_PATH = '/gate/signup' - -export function Step3({ - uiState, - uiDispatch, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch -}) { - const {_} = useLingui() - const theme = useTheme() - const submit = useSubmitCreateAccount(uiState, uiDispatch) - - const [completed, setCompleted] = React.useState(false) - - const stateParam = React.useMemo(() => nanoid(15), []) - const url = React.useMemo(() => { - const newUrl = new URL(uiState.serviceUrl) - newUrl.pathname = CAPTCHA_PATH - newUrl.searchParams.set( - 'handle', - createFullHandle(uiState.handle, uiState.userDomain), - ) - newUrl.searchParams.set('state', stateParam) - newUrl.searchParams.set('colorScheme', theme.colorScheme) - - console.log(newUrl) - - return newUrl.href - }, [ - uiState.serviceUrl, - uiState.handle, - uiState.userDomain, - stateParam, - theme.colorScheme, - ]) - - const onSuccess = React.useCallback( - (code: string) => { - setCompleted(true) - submit(code) - }, - [submit], - ) - - const onError = React.useCallback(() => { - uiDispatch({ - type: 'set-error', - value: _(msg`Error receiving captcha response.`), - }) - }, [_, uiDispatch]) - - return ( - <View> - <StepHeader uiState={uiState} title={_(msg`Complete the challenge`)} /> - <View style={[styles.container, completed && styles.center]}> - {!completed ? ( - <CaptchaWebView - url={url} - stateParam={stateParam} - uiState={uiState} - onSuccess={onSuccess} - onError={onError} - /> - ) : ( - <ActivityIndicator size="large" /> - )} - </View> - - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : undefined} - </View> - ) -} - -const styles = StyleSheet.create({ - error: { - borderRadius: 6, - marginTop: 10, - }, - // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. - touchable: { - ...(isWeb && {cursor: 'pointer'}), - }, - container: { - minHeight: 500, - width: '100%', - paddingBottom: 20, - overflow: 'hidden', - }, - center: { - alignItems: 'center', - justifyContent: 'center', - }, -}) diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx deleted file mode 100644 index a98b392d8..000000000 --- a/src/view/com/auth/create/StepHeader.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Text} from 'view/com/util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {Trans} from '@lingui/macro' -import {CreateAccountState} from './state' - -export function StepHeader({ - uiState, - title, - children, -}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) { - const pal = usePalette('default') - const numSteps = 3 - return ( - <View style={styles.container}> - <View> - <Text type="lg" style={[pal.textLight]}> - {uiState.step === 3 ? ( - <Trans>Last step!</Trans> - ) : ( - <Trans> - Step {uiState.step} of {numSteps} - </Trans> - )} - </Text> - - <Text style={[pal.text]} type="title-xl"> - {title} - </Text> - </View> - {children} - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 20, - }, -}) diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx deleted file mode 100644 index d3b075fdb..000000000 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React from 'react' -import {ScrollView, TouchableOpacity, View} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {UserAvatar} from '../../util/UserAvatar' -import {s, colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {styles} from './styles' -import {useSession, useSessionApi, SessionAccount} from '#/state/session' -import {useProfileQuery} from '#/state/queries/profile' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import * as Toast from '#/view/com/util/Toast' - -function AccountItem({ - account, - onSelect, - isCurrentAccount, -}: { - account: SessionAccount - onSelect: (account: SessionAccount) => void - isCurrentAccount: boolean -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {data: profile} = useProfileQuery({did: account.did}) - - const onPress = React.useCallback(() => { - onSelect(account) - }, [account, onSelect]) - - return ( - <TouchableOpacity - testID={`chooseAccountBtn-${account.handle}`} - key={account.did} - style={[pal.view, pal.border, styles.account]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={_(msg`Sign in as ${account.handle}`)} - accessibilityHint={_(msg`Double tap to sign in`)}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <View style={s.p10}> - <UserAvatar - avatar={profile?.avatar} - size={30} - type={profile?.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - <Text style={styles.accountText}> - <Text type="lg-bold" style={pal.text}> - {profile?.displayName || account.handle}{' '} - </Text> - <Text type="lg" style={[pal.textLight]}> - {account.handle} - </Text> - </Text> - {isCurrentAccount ? ( - <FontAwesomeIcon - icon="check" - size={16} - style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]} - /> - ) : ( - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - )} - </View> - </TouchableOpacity> - ) -} -export const ChooseAccountForm = ({ - onSelectAccount, - onPressBack, -}: { - onSelectAccount: (account?: SessionAccount) => void - onPressBack: () => void -}) => { - const {track, screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - const {accounts, currentAccount} = useSession() - const {initSession} = useSessionApi() - const {setShowLoggedOut} = useLoggedOutViewControls() - - React.useEffect(() => { - screen('Choose Account') - }, [screen]) - - const onSelect = React.useCallback( - async (account: SessionAccount) => { - if (account.accessJwt) { - if (account.did === currentAccount?.did) { - setShowLoggedOut(false) - Toast.show(_(msg`Already signed in as @${account.handle}`)) - } else { - await initSession(account) - track('Sign In', {resumedSession: true}) - setTimeout(() => { - Toast.show(_(msg`Signed in as @${account.handle}`)) - }, 100) - } - } else { - onSelectAccount(account) - } - }, - [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _], - ) - - return ( - <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> - <Text - type="2xl-medium" - style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}> - <Trans>Sign in as...</Trans> - </Text> - {accounts.map(account => ( - <AccountItem - key={account.did} - account={account} - onSelect={onSelect} - isCurrentAccount={account.did === currentAccount?.did} - /> - ))} - <TouchableOpacity - testID="chooseNewAccountBtn" - style={[pal.view, pal.border, styles.account, styles.accountLast]} - onPress={() => onSelectAccount(undefined)} - accessibilityRole="button" - accessibilityLabel={_(msg`Login to account that is not listed`)} - accessibilityHint=""> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <Text style={[styles.accountText, styles.accountTextOther]}> - <Text type="lg" style={pal.text}> - <Trans>Other account</Trans> - </Text> - </Text> - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - </View> - </TouchableOpacity> - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - </View> - </ScrollView> - ) -} diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx deleted file mode 100644 index 322da2b8f..000000000 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React, {useState, useEffect} from 'react' -import { - ActivityIndicator, - Keyboard, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import * as EmailValidator from 'email-validator' -import {BskyAgent} from '@atproto/api' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {toNiceDomain} from 'lib/strings/url-helpers' -import {isNetworkError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {cleanError} from 'lib/strings/errors' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {styles} from './styles' -import {useDialogControl} from '#/components/Dialog' - -import {ServerInputDialog} from '../server-input' - -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema - -export const ForgotPasswordForm = ({ - error, - serviceUrl, - serviceDescription, - setError, - setServiceUrl, - onPressBack, - onEmailSent, -}: { - error: string - serviceUrl: string - serviceDescription: ServiceDescription | undefined - setError: (v: string) => void - setServiceUrl: (v: string) => void - onPressBack: () => void - onEmailSent: () => void -}) => { - const pal = usePalette('default') - const theme = useTheme() - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [email, setEmail] = useState<string>('') - const {screen} = useAnalytics() - const {_} = useLingui() - const serverInputControl = useDialogControl() - - useEffect(() => { - screen('Signin:ForgotPassword') - }, [screen]) - - const onPressSelectService = React.useCallback(() => { - serverInputControl.open() - Keyboard.dismiss() - }, [serverInputControl]) - - const onPressNext = async () => { - if (!EmailValidator.validate(email)) { - return setError(_(msg`Your email appears to be invalid.`)) - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.requestPasswordReset({email}) - onEmailSent() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to request password reset', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - return ( - <> - <View> - <ServerInputDialog - control={serverInputControl} - onSelect={setServiceUrl} - /> - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> - <Trans>Reset password</Trans> - </Text> - <Text type="md" style={[pal.text, styles.instructions]}> - <Trans> - Enter the email you used to create your account. We'll send you a - "reset code" so you can set a new password. - </Trans> - </Text> - <View - testID="forgotPasswordView" - style={[pal.borderDark, pal.view, styles.group]}> - <TouchableOpacity - testID="forgotPasswordSelectServiceButton" - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]} - onPress={onPressSelectService} - accessibilityRole="button" - accessibilityLabel={_(msg`Hosting provider`)} - accessibilityHint={_( - msg`Sets hosting provider for password reset`, - )}> - <FontAwesomeIcon - icon="globe" - style={[pal.textLight, styles.groupContentIcon]} - /> - <Text style={[pal.text, styles.textInput]} numberOfLines={1}> - {toNiceDomain(serviceUrl)} - </Text> - <View style={[pal.btn, styles.textBtnFakeInnerBtn]}> - <FontAwesomeIcon - icon="pen" - size={12} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </TouchableOpacity> - <View style={[pal.borderDark, styles.groupContent]}> - <FontAwesomeIcon - icon="envelope" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="forgotPasswordEmail" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Email address`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoFocus - autoCorrect={false} - keyboardAppearance={theme.colorScheme} - value={email} - onChangeText={setEmail} - editable={!isProcessing} - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_(msg`Sets email for password reset`)} - /> - </View> - </View> - {error ? ( - <View style={styles.error}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> - </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> - </View> - </View> - ) : undefined} - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {!serviceDescription || isProcessing ? ( - <ActivityIndicator /> - ) : !email ? ( - <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> - <Trans>Next</Trans> - </Text> - ) : ( - <TouchableOpacity - testID="newPasswordButton" - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - </TouchableOpacity> - )} - {!serviceDescription || isProcessing ? ( - <Text type="xl" style={[pal.textLight, s.pl10]}> - <Trans>Processing...</Trans> - </Text> - ) : undefined} - </View> - <View - style={[ - s.flexRow, - s.alignCenter, - s.mt20, - s.mb20, - pal.border, - s.borderBottom1, - {alignSelf: 'center', width: '90%'}, - ]} - /> - <View style={[s.flexRow, s.justifyCenter]}> - <TouchableOpacity - testID="skipSendEmailButton" - onPress={onEmailSent} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl" style={[pal.link, s.pr5]}> - <Trans>Already have a code?</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - </> - ) -} diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx deleted file mode 100644 index bc931ac04..000000000 --- a/src/view/com/auth/login/Login.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, {useState, useEffect} from 'react' -import {KeyboardAvoidingView} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' -import {DEFAULT_SERVICE} from '#/lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {logger} from '#/logger' -import {ChooseAccountForm} from './ChooseAccountForm' -import {LoginForm} from './LoginForm' -import {ForgotPasswordForm} from './ForgotPasswordForm' -import {SetNewPasswordForm} from './SetNewPasswordForm' -import {PasswordUpdatedForm} from './PasswordUpdatedForm' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' -import {useSession, SessionAccount} from '#/state/session' -import {useServiceQuery} from '#/state/queries/service' -import {useLoggedOutView} from '#/state/shell/logged-out' - -enum Forms { - Login, - ChooseAccount, - ForgotPassword, - SetNewPassword, - PasswordUpdated, -} - -export const Login = ({onPressBack}: {onPressBack: () => void}) => { - const {_} = useLingui() - const pal = usePalette('default') - - const {accounts} = useSession() - const {track} = useAnalytics() - const {requestedAccountSwitchTo} = useLoggedOutView() - const requestedAccount = accounts.find( - a => a.did === requestedAccountSwitchTo, - ) - - const [error, setError] = useState<string>('') - const [serviceUrl, setServiceUrl] = useState<string>( - requestedAccount?.service || DEFAULT_SERVICE, - ) - const [initialHandle, setInitialHandle] = useState<string>( - requestedAccount?.handle || '', - ) - const [currentForm, setCurrentForm] = useState<Forms>( - requestedAccount - ? Forms.Login - : accounts.length - ? Forms.ChooseAccount - : Forms.Login, - ) - - const { - data: serviceDescription, - error: serviceError, - refetch: refetchService, - } = useServiceQuery(serviceUrl) - - const onSelectAccount = (account?: SessionAccount) => { - if (account?.service) { - setServiceUrl(account.service) - } - setInitialHandle(account?.handle || '') - setCurrentForm(Forms.Login) - } - - const gotoForm = (form: Forms) => () => { - setError('') - setCurrentForm(form) - } - - useEffect(() => { - if (serviceError) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - logger.warn(`Failed to fetch service description for ${serviceUrl}`, { - error: String(serviceError), - }) - } else { - setError('') - } - }, [serviceError, serviceUrl, _]) - - const onPressRetryConnect = () => refetchService() - const onPressForgotPassword = () => { - track('Signin:PressedForgotPassword') - setCurrentForm(Forms.ForgotPassword) - } - - return ( - <KeyboardAvoidingView testID="signIn" behavior="padding" style={pal.view}> - {currentForm === Forms.Login ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Sign in`)} - description={_(msg`Enter your username and password`)}> - <LoginForm - error={error} - serviceUrl={serviceUrl} - serviceDescription={serviceDescription} - initialHandle={initialHandle} - setError={setError} - setServiceUrl={setServiceUrl} - onPressBack={onPressBack} - onPressForgotPassword={onPressForgotPassword} - onPressRetryConnect={onPressRetryConnect} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.ChooseAccount ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Sign in as...`)} - description={_(msg`Select from an existing account`)}> - <ChooseAccountForm - onSelectAccount={onSelectAccount} - onPressBack={onPressBack} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.ForgotPassword ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Forgot Password`)} - description={_(msg`Let's get your password reset!`)}> - <ForgotPasswordForm - error={error} - serviceUrl={serviceUrl} - serviceDescription={serviceDescription} - setError={setError} - setServiceUrl={setServiceUrl} - onPressBack={gotoForm(Forms.Login)} - onEmailSent={gotoForm(Forms.SetNewPassword)} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.SetNewPassword ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Forgot Password`)} - description={_(msg`Let's get your password reset!`)}> - <SetNewPasswordForm - error={error} - serviceUrl={serviceUrl} - setError={setError} - onPressBack={gotoForm(Forms.ForgotPassword)} - onPasswordSet={gotoForm(Forms.PasswordUpdated)} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.PasswordUpdated ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Password updated`)} - description={_(msg`You can now sign in with your new password.`)}> - <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} /> - </LoggedOutLayout> - ) : undefined} - </KeyboardAvoidingView> - ) -} diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx deleted file mode 100644 index 3202d69c5..000000000 --- a/src/view/com/auth/login/LoginForm.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, {useState, useRef} from 'react' -import { - ActivityIndicator, - Keyboard, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {createFullHandle} from 'lib/strings/handles' -import {toNiceDomain} from 'lib/strings/url-helpers' -import {isNetworkError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {useSessionApi} from '#/state/session' -import {cleanError} from 'lib/strings/errors' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' -import {styles} from './styles' -import {useLingui} from '@lingui/react' -import {useDialogControl} from '#/components/Dialog' - -import {ServerInputDialog} from '../server-input' - -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema - -export const LoginForm = ({ - error, - serviceUrl, - serviceDescription, - initialHandle, - setError, - setServiceUrl, - onPressRetryConnect, - onPressBack, - onPressForgotPassword, -}: { - error: string - serviceUrl: string - serviceDescription: ServiceDescription | undefined - initialHandle: string - setError: (v: string) => void - setServiceUrl: (v: string) => void - onPressRetryConnect: () => void - onPressBack: () => void - onPressForgotPassword: () => void -}) => { - const {track} = useAnalytics() - const pal = usePalette('default') - const theme = useTheme() - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [identifier, setIdentifier] = useState<string>(initialHandle) - const [password, setPassword] = useState<string>('') - const passwordInputRef = useRef<TextInput>(null) - const {_} = useLingui() - const {login} = useSessionApi() - const serverInputControl = useDialogControl() - - const onPressSelectService = () => { - serverInputControl.open() - Keyboard.dismiss() - track('Signin:PressedSelectService') - } - - const onPressNext = async () => { - Keyboard.dismiss() - setError('') - setIsProcessing(true) - - try { - // try to guess the handle if the user just gave their own username - let fullIdent = identifier - if ( - !identifier.includes('@') && // not an email - !identifier.includes('.') && // not a domain - serviceDescription && - serviceDescription.availableUserDomains.length > 0 - ) { - let matched = false - for (const domain of serviceDescription.availableUserDomains) { - if (fullIdent.endsWith(domain)) { - matched = true - } - } - if (!matched) { - fullIdent = createFullHandle( - identifier, - serviceDescription.availableUserDomains[0], - ) - } - } - - // TODO remove double login - await login({ - service: serviceUrl, - identifier: fullIdent, - password, - }) - } catch (e: any) { - const errMsg = e.toString() - setIsProcessing(false) - if (errMsg.includes('Authentication Required')) { - logger.debug('Failed to login due to invalid credentials', { - error: errMsg, - }) - setError(_(msg`Invalid username or password`)) - } else if (isNetworkError(e)) { - logger.warn('Failed to login due to network error', {error: errMsg}) - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - logger.warn('Failed to login', {error: errMsg}) - setError(cleanError(errMsg)) - } - } - } - - const isReady = !!serviceDescription && !!identifier && !!password - return ( - <View testID="loginForm"> - <ServerInputDialog - control={serverInputControl} - onSelect={setServiceUrl} - /> - - <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> - <Trans>Sign into</Trans> - </Text> - <View style={[pal.borderDark, styles.group]}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <FontAwesomeIcon - icon="globe" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TouchableOpacity - testID="loginSelectServiceButton" - style={styles.textBtn} - onPress={onPressSelectService} - accessibilityRole="button" - accessibilityLabel={_(msg`Select service`)} - accessibilityHint={_(msg`Sets server for the Bluesky client`)}> - <Text type="xl" style={[pal.text, styles.textBtnLabel]}> - {toNiceDomain(serviceUrl)} - </Text> - <View style={[pal.btn, styles.textBtnFakeInnerBtn]}> - <FontAwesomeIcon - icon="pen" - size={12} - style={pal.textLight as FontAwesomeIconStyle} - /> - </View> - </TouchableOpacity> - </View> - </View> - <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> - <Trans>Account</Trans> - </Text> - <View style={[pal.borderDark, styles.group]}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <FontAwesomeIcon - icon="at" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="loginUsernameInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Username or email address`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoFocus - autoCorrect={false} - autoComplete="username" - returnKeyType="next" - textContentType="username" - onSubmitEditing={() => { - passwordInputRef.current?.focus() - }} - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field - keyboardAppearance={theme.colorScheme} - value={identifier} - onChangeText={str => - setIdentifier((str || '').toLowerCase().trim()) - } - editable={!isProcessing} - accessibilityLabel={_(msg`Username or email address`)} - accessibilityHint={_( - msg`Input the username or email address you used at signup`, - )} - /> - </View> - <View style={[pal.borderDark, styles.groupContent]}> - <FontAwesomeIcon - icon="lock" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="loginPasswordInput" - ref={passwordInputRef} - style={[pal.text, styles.textInput]} - placeholder={_(msg`Password`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoCorrect={false} - autoComplete="password" - returnKeyType="done" - enablesReturnKeyAutomatically={true} - keyboardAppearance={theme.colorScheme} - secureTextEntry={true} - textContentType="password" - clearButtonMode="while-editing" - value={password} - onChangeText={setPassword} - onSubmitEditing={onPressNext} - blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing - editable={!isProcessing} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={ - identifier === '' - ? _(msg`Input your password`) - : _(msg`Input the password tied to ${identifier}`) - } - /> - <TouchableOpacity - testID="forgotPasswordButton" - style={styles.textInputInnerBtn} - onPress={onPressForgotPassword} - accessibilityRole="button" - accessibilityLabel={_(msg`Forgot password`)} - accessibilityHint={_(msg`Opens password reset form`)}> - <Text style={pal.link}> - <Trans>Forgot</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - {error ? ( - <View style={styles.error}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> - </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> - </View> - </View> - ) : undefined} - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {!serviceDescription && error ? ( - <TouchableOpacity - testID="loginRetryButton" - onPress={onPressRetryConnect} - accessibilityRole="button" - accessibilityLabel={_(msg`Retry`)} - accessibilityHint={_(msg`Retries login`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Retry</Trans> - </Text> - </TouchableOpacity> - ) : !serviceDescription ? ( - <> - <ActivityIndicator /> - <Text type="xl" style={[pal.textLight, s.pl10]}> - <Trans>Connecting...</Trans> - </Text> - </> - ) : isProcessing ? ( - <ActivityIndicator /> - ) : isReady ? ( - <TouchableOpacity - testID="loginNextButton" - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - </TouchableOpacity> - ) : undefined} - </View> - </View> - ) -} diff --git a/src/view/com/auth/login/PasswordUpdatedForm.tsx b/src/view/com/auth/login/PasswordUpdatedForm.tsx deleted file mode 100644 index 71f750b14..000000000 --- a/src/view/com/auth/login/PasswordUpdatedForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, {useEffect} from 'react' -import {TouchableOpacity, View} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {styles} from './styles' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -export const PasswordUpdatedForm = ({ - onPressNext, -}: { - onPressNext: () => void -}) => { - const {screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - - useEffect(() => { - screen('Signin:PasswordUpdatedForm') - }, [screen]) - - return ( - <> - <View> - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> - <Trans>Password updated!</Trans> - </Text> - <Text type="lg" style={[pal.text, styles.instructions]}> - <Trans>You can now sign in with your new password.</Trans> - </Text> - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <View style={s.flex1} /> - <TouchableOpacity - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Close alert`)} - accessibilityHint={_(msg`Closes password update alert`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Okay</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - </> - ) -} diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx deleted file mode 100644 index 6d1584c86..000000000 --- a/src/view/com/auth/login/SetNewPasswordForm.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React, {useState, useEffect} from 'react' -import { - ActivityIndicator, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {BskyAgent} from '@atproto/api' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {isNetworkError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {cleanError} from 'lib/strings/errors' -import {checkAndFormatResetCode} from 'lib/strings/password' -import {logger} from '#/logger' -import {styles} from './styles' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -export const SetNewPasswordForm = ({ - error, - serviceUrl, - setError, - onPressBack, - onPasswordSet, -}: { - error: string - serviceUrl: string - setError: (v: string) => void - onPressBack: () => void - onPasswordSet: () => void -}) => { - const pal = usePalette('default') - const theme = useTheme() - const {screen} = useAnalytics() - const {_} = useLingui() - - useEffect(() => { - screen('Signin:SetNewPasswordForm') - }, [screen]) - - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [resetCode, setResetCode] = useState<string>('') - const [password, setPassword] = useState<string>('') - - const onPressNext = async () => { - // Check that the code is correct. We do this again just incase the user enters the code after their pw and we - // don't get to call onBlur first - const formattedCode = checkAndFormatResetCode(resetCode) - // TODO Better password strength check - if (!formattedCode || !password) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.resetPassword({ - token: formattedCode, - password, - }) - onPasswordSet() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to set new password', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - 'Unable to contact your service. Please check your Internet connection.', - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - const onBlur = () => { - const formattedCode = checkAndFormatResetCode(resetCode) - if (!formattedCode) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - setResetCode(formattedCode) - } - - return ( - <> - <View> - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> - <Trans>Set new password</Trans> - </Text> - <Text type="lg" style={[pal.text, styles.instructions]}> - <Trans> - You will receive an email with a "reset code." Enter that code here, - then enter your new password. - </Trans> - </Text> - <View - testID="newPasswordView" - style={[pal.view, pal.borderDark, styles.group]}> - <View - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <FontAwesomeIcon - icon="ticket" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="resetCodeInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Reset code`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoCorrect={false} - keyboardAppearance={theme.colorScheme} - autoComplete="off" - value={resetCode} - onChangeText={setResetCode} - onFocus={() => setError('')} - onBlur={onBlur} - editable={!isProcessing} - accessible={true} - accessibilityLabel={_(msg`Reset code`)} - accessibilityHint={_( - msg`Input code sent to your email for password reset`, - )} - /> - </View> - <View style={[pal.borderDark, styles.groupContent]}> - <FontAwesomeIcon - icon="lock" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="newPasswordInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`New password`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoCorrect={false} - autoComplete="new-password" - keyboardAppearance={theme.colorScheme} - secureTextEntry - value={password} - onChangeText={setPassword} - editable={!isProcessing} - accessible={true} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={_(msg`Input new password`)} - /> - </View> - </View> - {error ? ( - <View style={styles.error}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> - </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> - </View> - </View> - ) : undefined} - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {isProcessing ? ( - <ActivityIndicator /> - ) : !resetCode || !password ? ( - <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> - <Trans>Next</Trans> - </Text> - ) : ( - <TouchableOpacity - testID="setNewPasswordButton" - // Check the code before running the callback - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - </TouchableOpacity> - )} - {isProcessing ? ( - <Text type="xl" style={[pal.textLight, s.pl10]}> - <Trans>Updating...</Trans> - </Text> - ) : undefined} - </View> - </View> - </> - ) -} diff --git a/src/view/com/auth/login/styles.ts b/src/view/com/auth/login/styles.ts deleted file mode 100644 index 9dccc2803..000000000 --- a/src/view/com/auth/login/styles.ts +++ /dev/null @@ -1,118 +0,0 @@ -import {StyleSheet} from 'react-native' -import {colors} from 'lib/styles' -import {isWeb} from '#/platform/detection' - -export const styles = StyleSheet.create({ - screenTitle: { - marginBottom: 10, - marginHorizontal: 20, - }, - instructions: { - marginBottom: 20, - marginHorizontal: 20, - }, - group: { - borderWidth: 1, - borderRadius: 10, - marginBottom: 20, - marginHorizontal: 20, - }, - groupLabel: { - paddingHorizontal: 20, - paddingBottom: 5, - }, - groupContent: { - borderTopWidth: 1, - flexDirection: 'row', - alignItems: 'center', - }, - noTopBorder: { - borderTopWidth: 0, - }, - groupContentIcon: { - marginLeft: 10, - }, - account: { - borderTopWidth: 1, - paddingHorizontal: 20, - paddingVertical: 4, - }, - accountLast: { - borderBottomWidth: 1, - marginBottom: 20, - paddingVertical: 8, - }, - textInput: { - flex: 1, - width: '100%', - paddingVertical: 10, - paddingHorizontal: 12, - fontSize: 17, - letterSpacing: 0.25, - fontWeight: '400', - borderRadius: 10, - }, - textInputInnerBtn: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 6, - paddingHorizontal: 8, - marginHorizontal: 6, - }, - textBtn: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', - }, - textBtnLabel: { - flex: 1, - paddingVertical: 10, - paddingHorizontal: 12, - }, - textBtnFakeInnerBtn: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 6, - paddingVertical: 6, - paddingHorizontal: 8, - marginHorizontal: 6, - }, - accountText: { - flex: 1, - flexDirection: 'row', - alignItems: 'baseline', - paddingVertical: 10, - }, - accountTextOther: { - paddingLeft: 12, - }, - error: { - backgroundColor: colors.red4, - flexDirection: 'row', - alignItems: 'center', - marginTop: -5, - marginHorizontal: 20, - marginBottom: 15, - borderRadius: 8, - paddingHorizontal: 8, - paddingVertical: 8, - }, - errorIcon: { - borderWidth: 1, - borderColor: colors.white, - color: colors.white, - borderRadius: 30, - width: 16, - height: 16, - alignItems: 'center', - justifyContent: 'center', - marginRight: 5, - }, - dimmed: {opacity: 0.5}, - - maxHeight: { - // @ts-ignore web only -prf - maxHeight: isWeb ? '100vh' : undefined, - height: !isWeb ? '100%' : undefined, - }, -}) diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx index b26ac1dcb..0661b7a35 100644 --- a/src/view/com/auth/server-input/index.tsx +++ b/src/view/com/auth/server-input/index.tsx @@ -67,7 +67,7 @@ export function ServerInputDialog({ return ( <Dialog.Outer control={control} - nativeOptions={{sheet: {snapPoints: ['100%']}}} + nativeOptions={{sheet: {snapPoints: ['80', '100%']}}} onClose={onClose}> <Dialog.Handle /> |