From f5b39f2755bdf62b79a7310e58e70fbcc79ac854 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 13 Mar 2024 22:16:32 +0000 Subject: convert base login component and ChooseAccountForm --- src/alf/atoms.ts | 6 + src/screens/Login/ChooseAccountForm.tsx | 183 ++++++++++++++++++++++++++ src/screens/Login/index.tsx | 166 +++++++++++++++++++++++ src/view/com/auth/LoggedOut.tsx | 16 +-- src/view/com/auth/login/ChooseAccountForm.tsx | 158 ---------------------- src/view/com/auth/login/Login.tsx | 164 ----------------------- 6 files changed, 363 insertions(+), 330 deletions(-) create mode 100644 src/screens/Login/ChooseAccountForm.tsx create mode 100644 src/screens/Login/index.tsx delete mode 100644 src/view/com/auth/login/ChooseAccountForm.tsx delete mode 100644 src/view/com/auth/login/Login.tsx (limited to 'src') diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 5088e3aac..fef68ecab 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -154,6 +154,12 @@ export const atoms = { align_end: { alignItems: 'flex-end', }, + align_baseline: { + alignItems: 'baseline', + }, + align_stretch: { + alignItems: 'stretch', + }, self_auto: { alignSelf: 'auto', }, diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx new file mode 100644 index 000000000..99d1beb89 --- /dev/null +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -0,0 +1,183 @@ +import React from 'react' +import {ScrollView, TouchableOpacity, View} from 'react-native' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import flattenReactChildren from 'react-keyed-flatten-children' + +import {useAnalytics} from 'lib/analytics/analytics' +import {UserAvatar} from '../../view/com/util/UserAvatar' +import {colors} from 'lib/styles' +import {styles} from '../../view/com/auth/login/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' +import {Button} from '#/components/Button' +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' + +function Group({children}: {children: React.ReactNode}) { + const t = useTheme() + return ( + + {flattenReactChildren(children).map((child, i) => { + return React.isValidElement(child) ? ( + + {i > 0 ? ( + + ) : null} + {React.cloneElement(child, { + // @ts-ignore + style: { + borderRadius: 0, + borderWidth: 0, + }, + })} + + ) : null + })} + + ) +} + +function AccountItem({ + account, + onSelect, + isCurrentAccount, +}: { + account: SessionAccount + onSelect: (account: SessionAccount) => void + isCurrentAccount: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {data: profile} = useProfileQuery({did: account.did}) + + const onPress = React.useCallback(() => { + onSelect(account) + }, [account, onSelect]) + + return ( + + + + + + + + {profile?.displayName || account.handle}{' '} + + {account.handle} + + {isCurrentAccount ? ( + + ) : ( + + )} + + + ) +} +export const ChooseAccountForm = ({ + onSelectAccount, + onPressBack, +}: { + onSelectAccount: (account?: SessionAccount) => void + onPressBack: () => void +}) => { + const {track, screen} = useAnalytics() + const {_} = useLingui() + const t = useTheme() + 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 ( + + + Sign in as... + + + {accounts.map(account => ( + + ))} + onSelectAccount(undefined)} + accessibilityRole="button" + accessibilityLabel={_(msg`Login to account that is not listed`)} + accessibilityHint=""> + + + Other account + + + + + + + + + + + ) +} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx new file mode 100644 index 000000000..f2cfde550 --- /dev/null +++ b/src/screens/Login/index.tsx @@ -0,0 +1,166 @@ +import React from 'react' +import {useAnalytics} from '#/lib/analytics/analytics' +import {useLingui} from '@lingui/react' +import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' +import {SessionAccount, useSession} from '#/state/session' +import {DEFAULT_SERVICE} from '#/lib/constants' +import {useLoggedOutView} from '#/state/shell/logged-out' +import {useServiceQuery} from '#/state/queries/service' +import {msg} from '@lingui/macro' +import {logger} from '#/logger' +import {atoms as a} from '#/alf' +import {KeyboardAvoidingView} from 'react-native' +import {ChooseAccountForm} from './ChooseAccountForm' +import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm' +import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm' +import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm' +import {LoginForm} from '#/view/com/auth/login/LoginForm' + +enum Forms { + Login, + ChooseAccount, + ForgotPassword, + SetNewPassword, + PasswordUpdated, +} + +export const Login = ({onPressBack}: {onPressBack: () => void}) => { + const {_} = useLingui() + + const {accounts} = useSession() + const {track} = useAnalytics() + const {requestedAccountSwitchTo} = useLoggedOutView() + const requestedAccount = accounts.find( + acc => acc.did === requestedAccountSwitchTo, + ) + + const [error, setError] = React.useState('') + const [serviceUrl, setServiceUrl] = React.useState( + requestedAccount?.service || DEFAULT_SERVICE, + ) + const [initialHandle, setInitialHandle] = React.useState( + requestedAccount?.handle || '', + ) + const [currentForm, setCurrentForm] = React.useState( + 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) + } + + React.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) + } + + let content = null + let title = '' + let description = '' + + switch (currentForm) { + case Forms.Login: + title = _(msg`Sign in`) + description = _(msg`Enter your username and password`) + content = ( + + ) + break + case Forms.ChooseAccount: + title = _(msg`Sign in`) + description = _(msg`Select from an existing account`) + content = ( + + ) + break + case Forms.ForgotPassword: + title = _(msg`Forgot Password`) + description = _(msg`Let's get your password reset!`) + content = ( + + ) + break + case Forms.SetNewPassword: + title = _(msg`Forgot Password`) + description = _(msg`Let's get your password reset!`) + content = ( + + ) + break + case Forms.PasswordUpdated: + title = _(msg`Password updated`) + description = _(msg`You can now sign in with your new password.`) + content = + break + } + + return ( + + + {content} + + + ) +} diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 603abbab2..58604ec9e 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 {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 {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, diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx deleted file mode 100644 index 32cd8315d..000000000 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ /dev/null @@ -1,158 +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 ( - - - - - - - - {profile?.displayName || account.handle}{' '} - - - {account.handle} - - - {isCurrentAccount ? ( - - ) : ( - - )} - - - ) -} -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 ( - - - Sign in as... - - {accounts.map(account => ( - - ))} - onSelectAccount(undefined)} - accessibilityRole="button" - accessibilityLabel={_(msg`Login to account that is not listed`)} - accessibilityHint=""> - - - - Other account - - - - - - - - - Back - - - - - - ) -} 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('') - const [serviceUrl, setServiceUrl] = useState( - requestedAccount?.service || DEFAULT_SERVICE, - ) - const [initialHandle, setInitialHandle] = useState( - requestedAccount?.handle || '', - ) - const [currentForm, setCurrentForm] = useState( - 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 ( - - {currentForm === Forms.Login ? ( - - - - ) : undefined} - {currentForm === Forms.ChooseAccount ? ( - - - - ) : undefined} - {currentForm === Forms.ForgotPassword ? ( - - - - ) : undefined} - {currentForm === Forms.SetNewPassword ? ( - - - - ) : undefined} - {currentForm === Forms.PasswordUpdated ? ( - - - - ) : undefined} - - ) -} -- cgit 1.4.1 From 9f5289a1017856c303d7fefe28327b30eac3a909 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 13 Mar 2024 23:34:01 +0000 Subject: alf the login form --- src/components/forms/TextField.tsx | 8 +- src/components/icons/Lock.tsx | 5 + src/components/icons/Pencil.tsx | 5 + src/screens/Login/ChooseAccountForm.tsx | 95 +++++----- src/screens/Login/LoginForm.tsx | 292 ++++++++++++++++++++++++++++++ src/screens/Login/index.tsx | 4 +- src/view/com/auth/login/LoginForm.tsx | 298 ------------------------------- src/view/com/auth/server-input/index.tsx | 2 +- 8 files changed, 362 insertions(+), 347 deletions(-) create mode 100644 src/components/icons/Lock.tsx create mode 100644 src/components/icons/Pencil.tsx create mode 100644 src/screens/Login/LoginForm.tsx delete mode 100644 src/view/com/auth/login/LoginForm.tsx (limited to 'src') diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index b37f4bfae..3cffe5b2b 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -14,6 +14,7 @@ import {useTheme, atoms as a, web, android} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Props as SVGIconProps} from '#/components/icons/common' +import {mergeRefs} from '#/lib/merge-refs' const Context = React.createContext<{ inputRef: React.RefObject | null @@ -128,6 +129,7 @@ export type InputProps = Omit & { value: string onChangeText: (value: string) => void isInvalid?: boolean + inputRef?: React.RefObject } export function createInput(Component: typeof TextInput) { @@ -137,6 +139,7 @@ export function createInput(Component: typeof TextInput) { value, onChangeText, isInvalid, + inputRef, ...rest }: InputProps) { const t = useTheme() @@ -161,19 +164,22 @@ export function createInput(Component: typeof TextInput) { ) } + const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) + return ( <> { screen('Choose Account') @@ -133,50 +134,54 @@ export const ChooseAccountForm = ({ return ( - - Sign in as... - - - {accounts.map(account => ( - - ))} - onSelectAccount(undefined)} - accessibilityRole="button" - accessibilityLabel={_(msg`Login to account that is not listed`)} - accessibilityHint=""> - - - Other account - - - - - - - - + + + Sign in as... + + + {accounts.map(account => ( + + ))} + onSelectAccount(undefined)} + accessibilityRole="button" + accessibilityLabel={_(msg`Login to account that is not listed`)} + accessibilityHint=""> + + + Other account + + + + + + + + + ) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx new file mode 100644 index 000000000..8ac4fa359 --- /dev/null +++ b/src/screens/Login/LoginForm.tsx @@ -0,0 +1,292 @@ +import React, {useState, useRef} from 'react' +import { + ActivityIndicator, + Keyboard, + TextInput, + TouchableOpacity, + View, +} from 'react-native' +import {ComAtprotoServerDescribeServer} from '@atproto/api' +import {Trans, msg} from '@lingui/macro' + +import {useAnalytics} from 'lib/analytics/analytics' +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 {useSessionApi} from '#/state/session' +import {cleanError} from 'lib/strings/errors' +import {logger} from '#/logger' +import {styles} from '../../view/com/auth/login/styles' +import {useLingui} from '@lingui/react' +import {useDialogControl} from '#/components/Dialog' +import {ServerInputDialog} from '../../view/com/auth/server-input' +import {Button} from '#/components/Button' +import {isAndroid} from '#/platform/detection' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import * as TextField from '#/components/forms/TextField' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' + +type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema + +export const LoginForm = ({ + error, + serviceUrl, + serviceDescription, + initialHandle, + setError, + setServiceUrl, + onPressRetryConnect, + onPressBack, +}: { + 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 t = useTheme() + const [isProcessing, setIsProcessing] = useState(false) + const [identifier, setIdentifier] = useState(initialHandle) + const [password, setPassword] = useState('') + const passwordInputRef = useRef(null) + const {_} = useLingui() + const {login} = useSessionApi() + const serverInputControl = useDialogControl() + const {gtMobile} = useBreakpoints() + + 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 ( + + + + + + Hosting provider + + + + {toNiceDomain(serviceUrl)} + + + + + + + + Account + + + + { + passwordInputRef.current?.focus() + }} + blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field + value={identifier} + onChangeText={str => + setIdentifier((str || '').toLowerCase().trim()) + } + editable={!isProcessing} + accessibilityHint={_( + msg`Input the username or email address you used at signup`, + )} + /> + + + + + + + {/* + + Forgot + + */} + + + {error ? ( + + + + {error} + + + ) : undefined} + + + + {!serviceDescription && error ? ( + + ) : !serviceDescription ? ( + <> + + + Connecting... + + + ) : isProcessing ? ( + + ) : isReady ? ( + + ) : undefined} + + + ) +} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index f2cfde550..3bd2df60b 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {KeyboardAvoidingView} from 'react-native' import {useAnalytics} from '#/lib/analytics/analytics' import {useLingui} from '@lingui/react' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' @@ -9,12 +10,11 @@ import {useServiceQuery} from '#/state/queries/service' import {msg} from '@lingui/macro' import {logger} from '#/logger' import {atoms as a} from '#/alf' -import {KeyboardAvoidingView} from 'react-native' import {ChooseAccountForm} from './ChooseAccountForm' import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm' import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm' import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm' -import {LoginForm} from '#/view/com/auth/login/LoginForm' +import {LoginForm} from '#/screens/Login/LoginForm' enum Forms { Login, diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx deleted file mode 100644 index fdba9f203..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(false) - const [identifier, setIdentifier] = useState(initialHandle) - const [password, setPassword] = useState('') - const passwordInputRef = useRef(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 ( - - - - - Sign into - - - - - - - {toNiceDomain(serviceUrl)} - - - - - - - - - Account - - - - - { - 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`, - )} - /> - - - - - - - Forgot - - - - - {error ? ( - - - - - - {error} - - - ) : undefined} - - - - Back - - - - {!serviceDescription && error ? ( - - - Retry - - - ) : !serviceDescription ? ( - <> - - - Connecting... - - - ) : isProcessing ? ( - - ) : isReady ? ( - - - Next - - - ) : undefined} - - - ) -} diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx index 32b5a3141..81f4bdf93 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 ( -- cgit 1.4.1 From a06f6ada4e3905df49fe297d8393bb70ebca31be Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 13 Mar 2024 23:47:01 +0000 Subject: experiment: animate between screens --- src/screens/Login/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 3bd2df60b..028a497d2 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -2,6 +2,8 @@ import React from 'react' import {KeyboardAvoidingView} from 'react-native' import {useAnalytics} from '#/lib/analytics/analytics' import {useLingui} from '@lingui/react' +import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' + import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {SessionAccount, useSession} from '#/state/session' import {DEFAULT_SERVICE} from '#/lib/constants' @@ -159,7 +161,12 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { return ( - {content} + + {content} + ) -- cgit 1.4.1 From 8316f97e274504c3fc10dc22017ecbf958727c22 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 14 Mar 2024 18:25:39 +0000 Subject: fix collapsed input issue --- src/screens/Login/LoginForm.tsx | 305 +++++++++++++++++++++------------------- 1 file changed, 157 insertions(+), 148 deletions(-) (limited to 'src') diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 8ac4fa359..3089b3887 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -2,6 +2,7 @@ import React, {useState, useRef} from 'react' import { ActivityIndicator, Keyboard, + ScrollView, TextInput, TouchableOpacity, View, @@ -21,7 +22,7 @@ import {styles} from '../../view/com/auth/login/styles' import {useLingui} from '@lingui/react' import {useDialogControl} from '#/components/Dialog' import {ServerInputDialog} from '../../view/com/auth/server-input' -import {Button} from '#/components/Button' +import {Button, ButtonText} from '#/components/Button' import {isAndroid} from '#/platform/detection' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Text} from '#/components/Typography' @@ -43,6 +44,7 @@ export const LoginForm = ({ setServiceUrl, onPressRetryConnect, onPressBack, + onPressForgotPassword, }: { error: string serviceUrl: string @@ -129,164 +131,171 @@ export const LoginForm = ({ const isReady = !!serviceDescription && !!identifier && !!password return ( - - + + + - - - Hosting provider - - - - {toNiceDomain(serviceUrl)} - + + Hosting provider + + - + + {toNiceDomain(serviceUrl)} + + + + + + + + Account + + + + { + passwordInputRef.current?.focus() + }} + blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field + value={identifier} + onChangeText={str => + setIdentifier((str || '').toLowerCase().trim()) + } + editable={!isProcessing} + accessibilityHint={_( + msg`Input the username or email address you used at signup`, + )} /> - - - - - - Account - - - - { - passwordInputRef.current?.focus() - }} - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field - value={identifier} - onChangeText={str => - setIdentifier((str || '').toLowerCase().trim()) - } - editable={!isProcessing} - accessibilityHint={_( - msg`Input the username or email address you used at signup`, - )} - /> - - - - - - - {/* - - Forgot - - */} - - - {error ? ( - - - - {error} - + + + + + + + + + Forgot? + + + - ) : undefined} - - - - {!serviceDescription && error ? ( + {error ? ( + + + + {error} + + + ) : undefined} + - ) : !serviceDescription ? ( - <> + + {!serviceDescription && error ? ( + + ) : !serviceDescription ? ( + <> + + + Connecting... + + + ) : isProcessing ? ( - - Connecting... - - - ) : isProcessing ? ( - - ) : isReady ? ( - - ) : undefined} + ) : isReady ? ( + + ) : undefined} + - + ) } -- cgit 1.4.1 From a1fc95f30e8ff9f8903451b6f5d2daa318653167 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 15 Mar 2024 13:49:13 +0000 Subject: convert password reset flow --- assets/icons/ticket_stroke2_corner0_rounded.svg | 1 + src/alf/atoms.ts | 69 ++++++ src/components/forms/HostingProvider.tsx | 69 ++++++ src/components/icons/Ticket.tsx | 5 + src/screens/Login/ChooseAccountForm.tsx | 43 ++-- src/screens/Login/ForgotPasswordForm.tsx | 183 +++++++++++++++ src/screens/Login/FormContainer.tsx | 52 +++++ src/screens/Login/FormError.tsx | 34 +++ src/screens/Login/LoginForm.tsx | 297 ++++++++++-------------- src/screens/Login/PasswordUpdatedForm.tsx | 49 ++++ src/screens/Login/SetNewPasswordForm.tsx | 189 +++++++++++++++ src/screens/Login/index.tsx | 6 +- src/view/com/auth/login/ForgotPasswordForm.tsx | 228 ------------------ src/view/com/auth/login/PasswordUpdatedForm.tsx | 48 ---- src/view/com/auth/login/SetNewPasswordForm.tsx | 211 ----------------- src/view/com/auth/login/styles.ts | 118 ---------- 16 files changed, 803 insertions(+), 799 deletions(-) create mode 100644 assets/icons/ticket_stroke2_corner0_rounded.svg create mode 100644 src/components/forms/HostingProvider.tsx create mode 100644 src/components/icons/Ticket.tsx create mode 100644 src/screens/Login/ForgotPasswordForm.tsx create mode 100644 src/screens/Login/FormContainer.tsx create mode 100644 src/screens/Login/FormError.tsx create mode 100644 src/screens/Login/PasswordUpdatedForm.tsx create mode 100644 src/screens/Login/SetNewPasswordForm.tsx delete mode 100644 src/view/com/auth/login/ForgotPasswordForm.tsx delete mode 100644 src/view/com/auth/login/PasswordUpdatedForm.tsx delete mode 100644 src/view/com/auth/login/SetNewPasswordForm.tsx delete mode 100644 src/view/com/auth/login/styles.ts (limited to 'src') diff --git a/assets/icons/ticket_stroke2_corner0_rounded.svg b/assets/icons/ticket_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..184addc8e --- /dev/null +++ b/assets/icons/ticket_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index fef68ecab..33803b6fb 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -300,6 +300,9 @@ export const atoms = { /* * Padding */ + p_0: { + padding: 0, + }, p_2xs: { padding: tokens.space._2xs, }, @@ -330,6 +333,10 @@ export const atoms = { p_5xl: { padding: tokens.space._5xl, }, + px_0: { + paddingLeft: 0, + paddingRight: 0, + }, px_2xs: { paddingLeft: tokens.space._2xs, paddingRight: tokens.space._2xs, @@ -370,6 +377,10 @@ export const atoms = { paddingLeft: tokens.space._5xl, paddingRight: tokens.space._5xl, }, + py_0: { + paddingTop: 0, + paddingBottom: 0, + }, py_2xs: { paddingTop: tokens.space._2xs, paddingBottom: tokens.space._2xs, @@ -410,6 +421,9 @@ export const atoms = { paddingTop: tokens.space._5xl, paddingBottom: tokens.space._5xl, }, + pt_0: { + paddingTop: 0, + }, pt_2xs: { paddingTop: tokens.space._2xs, }, @@ -440,6 +454,9 @@ export const atoms = { pt_5xl: { paddingTop: tokens.space._5xl, }, + pb_0: { + paddingBottom: 0, + }, pb_2xs: { paddingBottom: tokens.space._2xs, }, @@ -470,6 +487,9 @@ export const atoms = { pb_5xl: { paddingBottom: tokens.space._5xl, }, + pl_0: { + paddingLeft: 0, + }, pl_2xs: { paddingLeft: tokens.space._2xs, }, @@ -500,6 +520,9 @@ export const atoms = { pl_5xl: { paddingLeft: tokens.space._5xl, }, + pr_0: { + paddingRight: 0, + }, pr_2xs: { paddingRight: tokens.space._2xs, }, @@ -534,6 +557,9 @@ export const atoms = { /* * Margin */ + m_0: { + margin: 0, + }, m_2xs: { margin: tokens.space._2xs, }, @@ -564,6 +590,13 @@ export const atoms = { m_5xl: { margin: tokens.space._5xl, }, + m_auto: { + margin: 'auto', + }, + mx_0: { + marginLeft: 0, + marginRight: 0, + }, mx_2xs: { marginLeft: tokens.space._2xs, marginRight: tokens.space._2xs, @@ -604,6 +637,14 @@ export const atoms = { marginLeft: tokens.space._5xl, marginRight: tokens.space._5xl, }, + mx_auto: { + marginLeft: 'auto', + marginRight: 'auto', + }, + my_0: { + marginTop: 0, + marginBottom: 0, + }, my_2xs: { marginTop: tokens.space._2xs, marginBottom: tokens.space._2xs, @@ -644,6 +685,13 @@ export const atoms = { marginTop: tokens.space._5xl, marginBottom: tokens.space._5xl, }, + my_auto: { + marginTop: 'auto', + marginBottom: 'auto', + }, + mt_0: { + marginTop: 0, + }, mt_2xs: { marginTop: tokens.space._2xs, }, @@ -674,6 +722,12 @@ export const atoms = { mt_5xl: { marginTop: tokens.space._5xl, }, + mt_auto: { + marginTop: 'auto', + }, + mb_0: { + marginBottom: 0, + }, mb_2xs: { marginBottom: tokens.space._2xs, }, @@ -704,6 +758,12 @@ export const atoms = { mb_5xl: { marginBottom: tokens.space._5xl, }, + mb_auto: { + marginBottom: 'auto', + }, + ml_0: { + marginLeft: 0, + }, ml_2xs: { marginLeft: tokens.space._2xs, }, @@ -734,6 +794,12 @@ export const atoms = { ml_5xl: { marginLeft: tokens.space._5xl, }, + ml_auto: { + marginLeft: 'auto', + }, + mr_0: { + marginRight: 0, + }, mr_2xs: { marginRight: tokens.space._2xs, }, @@ -764,4 +830,7 @@ export const atoms = { mr_5xl: { marginRight: tokens.space._5xl, }, + mr_auto: { + marginRight: 'auto', + }, } as const diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx new file mode 100644 index 000000000..df506b77c --- /dev/null +++ b/src/components/forms/HostingProvider.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {TouchableOpacity, View} from 'react-native' + +import {isAndroid} from '#/platform/detection' +import {atoms as a, useTheme} from '#/alf' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import * as TextField from './TextField' +import {useDialogControl} from '../Dialog' +import {Text} from '../Typography' +import {ServerInputDialog} from '#/view/com/auth/server-input' +import {toNiceDomain} from '#/lib/strings/url-helpers' + +export function HostingProvider({ + serviceUrl, + onSelectServiceUrl, + onOpenDialog, +}: { + serviceUrl: string + onSelectServiceUrl: (provider: string) => void + onOpenDialog?: () => void +}) { + const serverInputControl = useDialogControl() + const t = useTheme() + + const onPressSelectService = React.useCallback(() => { + serverInputControl.open() + if (onOpenDialog) { + onOpenDialog() + } + }, [onOpenDialog, serverInputControl]) + + return ( + <> + + + + {toNiceDomain(serviceUrl)} + + + + + + ) +} diff --git a/src/components/icons/Ticket.tsx b/src/components/icons/Ticket.tsx new file mode 100644 index 000000000..0df6b8120 --- /dev/null +++ b/src/components/icons/Ticket.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12c0-1.296.704-2.426 1.75-3.032a.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z', +}) diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index f5b3c2a86..7a3a4555b 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ScrollView, TouchableOpacity, View} from 'react-native' +import {TouchableOpacity, View} from 'react-native' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import flattenReactChildren from 'react-keyed-flatten-children' @@ -7,16 +7,17 @@ import flattenReactChildren from 'react-keyed-flatten-children' import {useAnalytics} from 'lib/analytics/analytics' import {UserAvatar} from '../../view/com/util/UserAvatar' import {colors} from 'lib/styles' -import {styles} from '../../view/com/auth/login/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' import {Button} from '#/components/Button' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import * as TextField from '#/components/forms/TextField' +import {FormContainer} from './FormContainer' function Group({children}: {children: React.ReactNode}) { const t = useTheme() @@ -106,7 +107,6 @@ export const ChooseAccountForm = ({ const {accounts, currentAccount} = useSession() const {initSession} = useSessionApi() const {setShowLoggedOut} = useLoggedOutViewControls() - const {gtMobile} = useBreakpoints() React.useEffect(() => { screen('Choose Account') @@ -133,12 +133,13 @@ export const ChooseAccountForm = ({ ) return ( - - - + Select account}> + + Sign in as... - + {accounts.map(account => ( - - - - - + + + + + ) } diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx new file mode 100644 index 000000000..fa674155a --- /dev/null +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -0,0 +1,183 @@ +import React, {useState, useEffect} from 'react' +import {ActivityIndicator, Keyboard, View} from 'react-native' +import {ComAtprotoServerDescribeServer} from '@atproto/api' +import * as EmailValidator from 'email-validator' +import {BskyAgent} from '@atproto/api' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import * as TextField from '#/components/forms/TextField' +import {HostingProvider} from '#/components/forms/HostingProvider' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import {atoms as a, useTheme} from '#/alf' +import {useAnalytics} from 'lib/analytics/analytics' +import {isNetworkError} from 'lib/strings/errors' +import {cleanError} from 'lib/strings/errors' +import {logger} from '#/logger' +import {Button, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {FormContainer} from './FormContainer' +import {FormError} from './FormError' + +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 t = useTheme() + const [isProcessing, setIsProcessing] = useState(false) + const [email, setEmail] = useState('') + const {screen} = useAnalytics() + const {_} = useLingui() + + useEffect(() => { + screen('Signin:ForgotPassword') + }, [screen]) + + const onPressSelectService = React.useCallback(() => { + Keyboard.dismiss() + }, []) + + 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 ( + Reset password}> + + + Hosting provider + + + + + + Email address + + + + + + + + + + Enter the email you used to create your account. We'll send you a + "reset code" so you can set a new password. + + + + + + + + {!serviceDescription || isProcessing ? ( + + ) : ( + + )} + {!serviceDescription || isProcessing ? ( + + Processing... + + ) : undefined} + + + + + + ) +} diff --git a/src/screens/Login/FormContainer.tsx b/src/screens/Login/FormContainer.tsx new file mode 100644 index 000000000..a08aa05b0 --- /dev/null +++ b/src/screens/Login/FormContainer.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { + ScrollView, + StyleSheet, + View, + type StyleProp, + type ViewStyle, +} from 'react-native' + +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import {isWeb} from '#/platform/detection' + +export function FormContainer({ + testID, + title, + children, + style, + contentContainerStyle, +}: { + testID?: string + title?: React.ReactNode + children: React.ReactNode + style?: StyleProp + contentContainerStyle?: StyleProp +}) { + const {gtMobile} = useBreakpoints() + const t = useTheme() + return ( + + + {title && !gtMobile && ( + + {title} + + )} + {children} + + + ) +} + +const styles = StyleSheet.create({ + maxHeight: { + // @ts-ignore web only -prf + maxHeight: isWeb ? '100vh' : undefined, + height: !isWeb ? '100%' : undefined, + }, +}) diff --git a/src/screens/Login/FormError.tsx b/src/screens/Login/FormError.tsx new file mode 100644 index 000000000..3c6a8649d --- /dev/null +++ b/src/screens/Login/FormError.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' + +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {Text} from '#/components/Typography' +import {atoms as a, useTheme} from '#/alf' +import {colors} from '#/lib/styles' + +export function FormError({error}: {error?: string}) { + const t = useTheme() + + if (!error) return null + + return ( + + + + {error} + + + ) +} + +const styles = StyleSheet.create({ + error: { + backgroundColor: colors.red4, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 15, + borderRadius: 8, + paddingHorizontal: 8, + paddingVertical: 8, + }, +}) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 3089b3887..580155281 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -2,36 +2,29 @@ import React, {useState, useRef} from 'react' import { ActivityIndicator, Keyboard, - ScrollView, TextInput, TouchableOpacity, View, } from 'react-native' import {ComAtprotoServerDescribeServer} from '@atproto/api' import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useAnalytics} from 'lib/analytics/analytics' -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 {useSessionApi} from '#/state/session' import {cleanError} from 'lib/strings/errors' import {logger} from '#/logger' -import {styles} from '../../view/com/auth/login/styles' -import {useLingui} from '@lingui/react' -import {useDialogControl} from '#/components/Dialog' -import {ServerInputDialog} from '../../view/com/auth/server-input' import {Button, ButtonText} from '#/components/Button' -import {isAndroid} from '#/platform/detection' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' import * as TextField from '#/components/forms/TextField' import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' -import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' -import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {HostingProvider} from '#/components/forms/HostingProvider' +import {FormContainer} from './FormContainer' +import {FormError} from './FormError' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -64,14 +57,11 @@ export const LoginForm = ({ const passwordInputRef = useRef(null) const {_} = useLingui() const {login} = useSessionApi() - const serverInputControl = useDialogControl() - const {gtMobile} = useBreakpoints() - const onPressSelectService = () => { - serverInputControl.open() + const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() track('Signin:PressedSelectService') - } + }, [track]) const onPressNext = async () => { Keyboard.dismiss() @@ -131,171 +121,138 @@ export const LoginForm = ({ const isReady = !!serviceDescription && !!identifier && !!password return ( - - - Sign in}> + + + Hosting provider + + - - - - Hosting provider - + + + + Account + + + + { + passwordInputRef.current?.focus() + }} + blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field + value={identifier} + onChangeText={str => + setIdentifier((str || '').toLowerCase().trim()) + } + editable={!isProcessing} + accessibilityHint={_( + msg`Input the username or email address you used at signup`, + )} + /> + + + + + + - - {toNiceDomain(serviceUrl)} - - - + t.atoms.bg_contrast_100, + {marginLeft: 'auto', left: 6, padding: 6}, + a.z_10, + ]}> + + Forgot? + - - - - Account - - - - { - passwordInputRef.current?.focus() - }} - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field - value={identifier} - onChangeText={str => - setIdentifier((str || '').toLowerCase().trim()) - } - editable={!isProcessing} - accessibilityHint={_( - msg`Input the username or email address you used at signup`, - )} - /> - - - - - - - - - Forgot? - - - - - {error ? ( - - - - {error} - - - ) : undefined} - + + + + + + + {!serviceDescription && error ? ( - - {!serviceDescription && error ? ( - - ) : !serviceDescription ? ( - <> - - - Connecting... - - - ) : isProcessing ? ( + ) : !serviceDescription ? ( + <> - ) : isReady ? ( - - ) : undefined} - + + Connecting... + + + ) : isProcessing ? ( + + ) : isReady ? ( + + ) : undefined} - + ) } diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx new file mode 100644 index 000000000..218cab539 --- /dev/null +++ b/src/screens/Login/PasswordUpdatedForm.tsx @@ -0,0 +1,49 @@ +import React, {useEffect} from 'react' +import {View} from 'react-native' +import {useAnalytics} from 'lib/analytics/analytics' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {FormContainer} from './FormContainer' +import {Button, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {atoms as a, useBreakpoints} from '#/alf' + +export const PasswordUpdatedForm = ({ + onPressNext, +}: { + onPressNext: () => void +}) => { + const {screen} = useAnalytics() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + + useEffect(() => { + screen('Signin:PasswordUpdatedForm') + }, [screen]) + + return ( + + + Password updated! + + + You can now sign in with your new password. + + + + + + ) +} diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx new file mode 100644 index 000000000..2685ad5ee --- /dev/null +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -0,0 +1,189 @@ +import React, {useState, useEffect} from 'react' +import {ActivityIndicator, View} from 'react-native' +import {BskyAgent} from '@atproto/api' +import {useAnalytics} from 'lib/analytics/analytics' + +import {isNetworkError} from 'lib/strings/errors' +import {cleanError} from 'lib/strings/errors' +import {checkAndFormatResetCode} from 'lib/strings/password' +import {logger} from '#/logger' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {FormContainer} from './FormContainer' +import {Text} from '#/components/Typography' +import * as TextField from '#/components/forms/TextField' +import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' +import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' +import {Button, ButtonText} from '#/components/Button' +import {useTheme, atoms as a} from '#/alf' +import {FormError} from './FormError' + +export const SetNewPasswordForm = ({ + error, + serviceUrl, + setError, + onPressBack, + onPasswordSet, +}: { + error: string + serviceUrl: string + setError: (v: string) => void + onPressBack: () => void + onPasswordSet: () => void +}) => { + const {screen} = useAnalytics() + const {_} = useLingui() + const t = useTheme() + + useEffect(() => { + screen('Signin:SetNewPasswordForm') + }, [screen]) + + const [isProcessing, setIsProcessing] = useState(false) + const [resetCode, setResetCode] = useState('') + const [password, setPassword] = useState('') + + const onPressNext = async () => { + onPasswordSet() + if (Math.random() > 0) return + // 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 ( + Set new password}> + + + You will receive an email with a "reset code." Enter that code here, + then enter your new password. + + + + + Reset code + + + setError('')} + onBlur={onBlur} + editable={!isProcessing} + accessibilityHint={_( + msg`Input code sent to your email for password reset`, + )} + /> + + + + + New password + + + + + + + + + + {isProcessing ? ( + + ) : ( + + )} + {isProcessing ? ( + + Updating... + + ) : undefined} + + + ) +} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 028a497d2..10edb3eb6 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -13,9 +13,9 @@ import {msg} from '@lingui/macro' import {logger} from '#/logger' import {atoms as a} from '#/alf' import {ChooseAccountForm} from './ChooseAccountForm' -import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm' -import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm' -import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm' +import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' +import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' +import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' import {LoginForm} from '#/screens/Login/LoginForm' enum Forms { 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(false) - const [email, setEmail] = useState('') - 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 ( - <> - - - - Reset password - - - - Enter the email you used to create your account. We'll send you a - "reset code" so you can set a new password. - - - - - - - {toNiceDomain(serviceUrl)} - - - - - - - - - - - {error ? ( - - - - - - {error} - - - ) : undefined} - - - - Back - - - - {!serviceDescription || isProcessing ? ( - - ) : !email ? ( - - Next - - ) : ( - - - Next - - - )} - {!serviceDescription || isProcessing ? ( - - Processing... - - ) : undefined} - - - - - - Already have a code? - - - - - - ) -} 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 ( - <> - - - Password updated! - - - You can now sign in with your new password. - - - - - - Okay - - - - - - ) -} 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(false) - const [resetCode, setResetCode] = useState('') - const [password, setPassword] = useState('') - - 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 ( - <> - - - Set new password - - - - You will receive an email with a "reset code." Enter that code here, - then enter your new password. - - - - - - setError('')} - onBlur={onBlur} - editable={!isProcessing} - accessible={true} - accessibilityLabel={_(msg`Reset code`)} - accessibilityHint={_( - msg`Input code sent to your email for password reset`, - )} - /> - - - - - - - {error ? ( - - - - - - {error} - - - ) : undefined} - - - - Back - - - - {isProcessing ? ( - - ) : !resetCode || !password ? ( - - Next - - ) : ( - - - Next - - - )} - {isProcessing ? ( - - Updating... - - ) : undefined} - - - - ) -} 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, - }, -}) -- cgit 1.4.1 From 7d69570a44d38278233f17f74a2239edd8ed81de Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 15 Mar 2024 13:53:57 +0000 Subject: run ticket through svgomg --- assets/icons/ticket_stroke2_corner0_rounded.svg | 2 +- src/components/icons/Ticket.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/assets/icons/ticket_stroke2_corner0_rounded.svg b/assets/icons/ticket_stroke2_corner0_rounded.svg index 184addc8e..a45a90ae5 100644 --- a/assets/icons/ticket_stroke2_corner0_rounded.svg +++ b/assets/icons/ticket_stroke2_corner0_rounded.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/components/icons/Ticket.tsx b/src/components/icons/Ticket.tsx index 0df6b8120..1a8059c2a 100644 --- a/src/components/icons/Ticket.tsx +++ b/src/components/icons/Ticket.tsx @@ -1,5 +1,5 @@ import {createSinglePathSVG} from './TEMPLATE' export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12c0-1.296.704-2.426 1.75-3.032a.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z', + path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z', }) -- cgit 1.4.1 From e31fcb1a6d8820fdbd05b34aa169839214b7a240 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 15 Mar 2024 13:54:23 +0000 Subject: reenable set new password button --- src/screens/Login/SetNewPasswordForm.tsx | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index 2685ad5ee..8fbd798c8 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -44,8 +44,6 @@ export const SetNewPasswordForm = ({ const [password, setPassword] = useState('') const onPressNext = async () => { - onPasswordSet() - if (Math.random() > 0) return // 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) -- cgit 1.4.1 From 0f67be362751f4f65bfca473691c27acb816e05c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 15 Mar 2024 14:21:15 +0000 Subject: animate transitions on web only --- src/screens/Login/ScreenTransition.tsx | 10 ++++++++++ src/screens/Login/ScreenTransition.web.tsx | 1 + src/screens/Login/index.tsx | 9 ++------- 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 src/screens/Login/ScreenTransition.tsx create mode 100644 src/screens/Login/ScreenTransition.web.tsx (limited to 'src') diff --git a/src/screens/Login/ScreenTransition.tsx b/src/screens/Login/ScreenTransition.tsx new file mode 100644 index 000000000..ab0a22367 --- /dev/null +++ b/src/screens/Login/ScreenTransition.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' + +export function ScreenTransition({children}: {children: React.ReactNode}) { + return ( + + {children} + + ) +} diff --git a/src/screens/Login/ScreenTransition.web.tsx b/src/screens/Login/ScreenTransition.web.tsx new file mode 100644 index 000000000..4583720aa --- /dev/null +++ b/src/screens/Login/ScreenTransition.web.tsx @@ -0,0 +1 @@ +export {Fragment as ScreenTransition} from 'react' diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 10edb3eb6..da392569a 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -2,7 +2,6 @@ import React from 'react' import {KeyboardAvoidingView} from 'react-native' import {useAnalytics} from '#/lib/analytics/analytics' import {useLingui} from '@lingui/react' -import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {SessionAccount, useSession} from '#/state/session' @@ -17,6 +16,7 @@ import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' import {LoginForm} from '#/screens/Login/LoginForm' +import {ScreenTransition} from './ScreenTransition' enum Forms { Login, @@ -161,12 +161,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { return ( - - {content} - + {content} ) -- cgit 1.4.1 From 273fe3e8e9f3ba1807d9f5163c02db9db58b0c22 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 15 Mar 2024 14:21:38 +0000 Subject: scrollview persist taps, autoFocus code --- src/screens/Login/FormContainer.tsx | 3 ++- src/screens/Login/SetNewPasswordForm.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/screens/Login/FormContainer.tsx b/src/screens/Login/FormContainer.tsx index a08aa05b0..cd17d06d7 100644 --- a/src/screens/Login/FormContainer.tsx +++ b/src/screens/Login/FormContainer.tsx @@ -29,7 +29,8 @@ export function FormContainer({ return ( + style={[styles.maxHeight, contentContainerStyle]} + keyboardShouldPersistTaps="handled"> {title && !gtMobile && ( diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index 8fbd798c8..0bfe72244 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -113,6 +113,7 @@ export const SetNewPasswordForm = ({ testID="resetCodeInput" label={_(msg`Looks like XXXXX-XXXXX`)} autoCapitalize="none" + autoFocus={true} autoCorrect={false} autoComplete="off" value={resetCode} -- cgit 1.4.1 From d2a11f3344149a299372f0a7dcd01de9f58ef9a1 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 15 Mar 2024 22:08:06 +0000 Subject: translate error --- src/screens/Login/SetNewPasswordForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index 0bfe72244..be0732483 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -73,7 +73,9 @@ export const SetNewPasswordForm = ({ setIsProcessing(false) if (isNetworkError(e)) { setError( - 'Unable to contact your service. Please check your Internet connection.', + _( + msg`Unable to contact your service. Please check your Internet connection.`, + ), ) } else { setError(cleanError(errMsg)) -- cgit 1.4.1 From 08d12d9a3df1fa062ecc4c67a0a2f686eba4c7c3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 15:28:06 +0000 Subject: move FormError to components/forms --- src/components/forms/FormError.tsx | 34 ++++++++++++++++++++++++++++++++ src/screens/Login/ForgotPasswordForm.tsx | 2 +- src/screens/Login/FormError.tsx | 34 -------------------------------- src/screens/Login/LoginForm.tsx | 2 +- src/screens/Login/SetNewPasswordForm.tsx | 2 +- 5 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 src/components/forms/FormError.tsx delete mode 100644 src/screens/Login/FormError.tsx (limited to 'src') diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx new file mode 100644 index 000000000..3c6a8649d --- /dev/null +++ b/src/components/forms/FormError.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' + +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {Text} from '#/components/Typography' +import {atoms as a, useTheme} from '#/alf' +import {colors} from '#/lib/styles' + +export function FormError({error}: {error?: string}) { + const t = useTheme() + + if (!error) return null + + return ( + + + + {error} + + + ) +} + +const styles = StyleSheet.create({ + error: { + backgroundColor: colors.red4, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 15, + borderRadius: 8, + paddingHorizontal: 8, + paddingVertical: 8, + }, +}) diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx index fa674155a..ab9d02536 100644 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -17,7 +17,7 @@ import {logger} from '#/logger' import {Button, ButtonText} from '#/components/Button' import {Text} from '#/components/Typography' import {FormContainer} from './FormContainer' -import {FormError} from './FormError' +import {FormError} from '#/components/forms/FormError' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema diff --git a/src/screens/Login/FormError.tsx b/src/screens/Login/FormError.tsx deleted file mode 100644 index 3c6a8649d..000000000 --- a/src/screens/Login/FormError.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' - -import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' -import {Text} from '#/components/Typography' -import {atoms as a, useTheme} from '#/alf' -import {colors} from '#/lib/styles' - -export function FormError({error}: {error?: string}) { - const t = useTheme() - - if (!error) return null - - return ( - - - - {error} - - - ) -} - -const styles = StyleSheet.create({ - error: { - backgroundColor: colors.red4, - flexDirection: 'row', - alignItems: 'center', - marginBottom: 15, - borderRadius: 8, - paddingHorizontal: 8, - paddingVertical: 8, - }, -}) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 580155281..ee47aa41d 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -24,7 +24,7 @@ import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' import {HostingProvider} from '#/components/forms/HostingProvider' import {FormContainer} from './FormContainer' -import {FormError} from './FormError' +import {FormError} from '#/components/forms/FormError' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index be0732483..678440cf4 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -16,7 +16,7 @@ import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' import {Button, ButtonText} from '#/components/Button' import {useTheme, atoms as a} from '#/alf' -import {FormError} from './FormError' +import {FormError} from '#/components/forms/FormError' export const SetNewPasswordForm = ({ error, -- cgit 1.4.1 From 49a392bfb34b91ba971f32cb25e41b0491c45fc2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 15:29:25 +0000 Subject: rm useless function wrap --- src/screens/Login/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index da392569a..8dee06185 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -85,7 +85,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } }, [serviceError, serviceUrl, _]) - const onPressRetryConnect = () => refetchService() const onPressForgotPassword = () => { track('Signin:PressedForgotPassword') setCurrentForm(Forms.ForgotPassword) @@ -109,7 +108,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { setServiceUrl={setServiceUrl} onPressBack={onPressBack} onPressForgotPassword={onPressForgotPassword} - onPressRetryConnect={onPressRetryConnect} + onPressRetryConnect={refetchService} /> ) break -- cgit 1.4.1 From b6903419a1112bae5397a398756e46a46afcf65f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 15:31:00 +0000 Subject: simplify gotoForm --- src/screens/Login/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 8dee06185..f7a0e29e9 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -65,7 +65,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { setCurrentForm(Forms.Login) } - const gotoForm = (form: Forms) => () => { + const gotoForm = (form: Forms) => { setError('') setCurrentForm(form) } @@ -132,8 +132,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { serviceDescription={serviceDescription} setError={setError} setServiceUrl={setServiceUrl} - onPressBack={gotoForm(Forms.Login)} - onEmailSent={gotoForm(Forms.SetNewPassword)} + onPressBack={() => gotoForm(Forms.Login)} + onEmailSent={() => gotoForm(Forms.SetNewPassword)} /> ) break @@ -145,15 +145,17 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { error={error} serviceUrl={serviceUrl} setError={setError} - onPressBack={gotoForm(Forms.ForgotPassword)} - onPasswordSet={gotoForm(Forms.PasswordUpdated)} + onPressBack={() => gotoForm(Forms.ForgotPassword)} + onPasswordSet={() => gotoForm(Forms.PasswordUpdated)} /> ) break case Forms.PasswordUpdated: title = _(msg`Password updated`) description = _(msg`You can now sign in with your new password.`) - content = + content = ( + gotoForm(Forms.Login)} /> + ) break } -- cgit 1.4.1 From a1c4f19731878f7026d398d28e475bbeb7de824a Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 19 Mar 2024 12:47:46 -0700 Subject: Use ALF for signup flow, improve a11y of signup (#3151) * Use ALF for signup flow, improve a11y of signup * adjust padding * rm log * org imports * clarify allowance of hyphens Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * fix a few accessibility items * Standardise date input across platforms (#3223) * make the date input consistent across platforms * integrate into new signup form * rm log * add transitions * show correct # of steps * use `FormError` * animate buttons * use `ScreenTransition` * fix android text overflow via flex -> flex_1 * change button color * (android) make date input the same height as others * fix deps * fix deps --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Samuel Newman --- assets/icons/calendar_stroke2_corner0_rounded.svg | 1 + assets/icons/envelope_stroke2_corner0_rounded.svg | 1 + assets/icons/lock_stroke2_corner0_rounded.svg | 1 + assets/icons/pencil_stroke2_corner0_rounded.svg | 1 + src/components/forms/DateField/index.android.tsx | 73 ++--- src/components/forms/DateField/index.shared.tsx | 99 +++++++ src/components/forms/DateField/index.tsx | 59 +++- src/components/forms/DateField/index.web.tsx | 4 + src/components/forms/DateField/types.ts | 1 + src/components/forms/TextField.tsx | 6 +- src/components/icons/Calendar.tsx | 5 + src/components/icons/Envelope.tsx | 5 + src/lib/strings/handles.ts | 2 + src/screens/Signup/StepCaptcha.tsx | 94 +++++++ src/screens/Signup/StepHandle.tsx | 134 +++++++++ src/screens/Signup/StepInfo.tsx | 145 ++++++++++ src/screens/Signup/index.tsx | 225 +++++++++++++++ src/screens/Signup/state.ts | 320 ++++++++++++++++++++++ src/view/com/auth/LoggedOut.tsx | 4 +- src/view/com/auth/create/CaptchaWebView.tsx | 14 +- src/view/com/auth/create/CreateAccount.tsx | 230 ---------------- src/view/com/auth/create/Policies.tsx | 14 +- src/view/com/auth/create/Step1.tsx | 261 ------------------ src/view/com/auth/create/Step2.tsx | 140 ---------- src/view/com/auth/create/Step3.tsx | 114 -------- src/view/com/auth/create/StepHeader.tsx | 44 --- 26 files changed, 1126 insertions(+), 871 deletions(-) create mode 100644 assets/icons/calendar_stroke2_corner0_rounded.svg create mode 100644 assets/icons/envelope_stroke2_corner0_rounded.svg create mode 100644 assets/icons/lock_stroke2_corner0_rounded.svg create mode 100644 assets/icons/pencil_stroke2_corner0_rounded.svg create mode 100644 src/components/forms/DateField/index.shared.tsx create mode 100644 src/components/icons/Calendar.tsx create mode 100644 src/components/icons/Envelope.tsx create mode 100644 src/screens/Signup/StepCaptcha.tsx create mode 100644 src/screens/Signup/StepHandle.tsx create mode 100644 src/screens/Signup/StepInfo.tsx create mode 100644 src/screens/Signup/index.tsx create mode 100644 src/screens/Signup/state.ts delete mode 100644 src/view/com/auth/create/CreateAccount.tsx delete mode 100644 src/view/com/auth/create/Step1.tsx delete mode 100644 src/view/com/auth/create/Step2.tsx delete mode 100644 src/view/com/auth/create/Step3.tsx delete mode 100644 src/view/com/auth/create/StepHeader.tsx (limited to 'src') diff --git a/assets/icons/calendar_stroke2_corner0_rounded.svg b/assets/icons/calendar_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..703f389db --- /dev/null +++ b/assets/icons/calendar_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/envelope_stroke2_corner0_rounded.svg b/assets/icons/envelope_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..c3ab45980 --- /dev/null +++ b/assets/icons/envelope_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/lock_stroke2_corner0_rounded.svg b/assets/icons/lock_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..8b094ba5e --- /dev/null +++ b/assets/icons/lock_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/pencil_stroke2_corner0_rounded.svg b/assets/icons/pencil_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..734198989 --- /dev/null +++ b/assets/icons/pencil_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index 451810a5e..35c2459f0 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -1,19 +1,12 @@ import React from 'react' -import {View, Pressable} from 'react-native' -import {useTheme, atoms} from '#/alf' -import {Text} from '#/components/Typography' -import {useInteractionState} from '#/components/hooks/useInteractionState' +import {useTheme} from '#/alf' import * as TextField from '#/components/forms/TextField' -import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' - import {DateFieldProps} from '#/components/forms/DateField/types' -import { - localizeDate, - toSimpleDateString, -} from '#/components/forms/DateField/utils' +import {toSimpleDateString} from '#/components/forms/DateField/utils' import DatePicker from 'react-native-date-picker' import {isAndroid} from 'platform/detection' +import {DateFieldButton} from './index.shared' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -24,18 +17,10 @@ export function DateField({ label, isInvalid, testID, + accessibilityHint, }: DateFieldProps) { const t = useTheme() const [open, setOpen] = React.useState(false) - const { - state: pressed, - onIn: onPressIn, - onOut: onPressOut, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - - const {chromeFocus, chromeError, chromeErrorHover} = - TextField.useSharedInputStyles() const onChangeInternal = React.useCallback( (date: Date) => { @@ -47,45 +32,23 @@ export function DateField({ [onChangeDate, setOpen], ) + const onPress = React.useCallback(() => { + setOpen(true) + }, []) + const onCancel = React.useCallback(() => { setOpen(false) }, []) return ( - - setOpen(true)} - onPressIn={onPressIn} - onPressOut={onPressOut} - onFocus={onFocus} - onBlur={onBlur} - style={[ - { - paddingTop: 16, - paddingBottom: 16, - borderColor: 'transparent', - borderWidth: 2, - }, - atoms.flex_row, - atoms.flex_1, - atoms.w_full, - atoms.px_lg, - atoms.rounded_sm, - t.atoms.bg_contrast_50, - focused || pressed ? chromeFocus : {}, - isInvalid ? chromeError : {}, - isInvalid && (focused || pressed) ? chromeErrorHover : {}, - ]}> - - - - {localizeDate(value)} - - + <> + {open && ( )} - + ) } diff --git a/src/components/forms/DateField/index.shared.tsx b/src/components/forms/DateField/index.shared.tsx new file mode 100644 index 000000000..29b3e8cb6 --- /dev/null +++ b/src/components/forms/DateField/index.shared.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import {View, Pressable} from 'react-native' + +import {atoms as a, android, useTheme, web} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import * as TextField from '#/components/forms/TextField' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' +import {localizeDate} from './utils' + +// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press +// iOS: open a dialog with an inline date picker +// Android: open the date picker modal + +export function DateFieldButton({ + label, + value, + onPress, + isInvalid, + accessibilityHint, +}: { + label: string + value: string + onPress: () => void + isInvalid?: boolean + accessibilityHint?: string +}) { + const t = useTheme() + + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = + TextField.useSharedInputStyles() + + return ( + + + + + {localizeDate(value)} + + + + ) +} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index 49e47a01e..22fa3a9f5 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -1,11 +1,16 @@ import React from 'react' import {View} from 'react-native' -import {useTheme, atoms} from '#/alf' +import {useTheme, atoms as a} from '#/alf' import * as TextField from '#/components/forms/TextField' import {toSimpleDateString} from '#/components/forms/DateField/utils' import {DateFieldProps} from '#/components/forms/DateField/types' import DatePicker from 'react-native-date-picker' +import * as Dialog from '#/components/Dialog' +import {DateFieldButton} from './index.shared' +import {Button, ButtonText} from '#/components/Button' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -22,8 +27,12 @@ export function DateField({ onChangeDate, testID, label, + isInvalid, + accessibilityHint, }: DateFieldProps) { + const {_} = useLingui() const t = useTheme() + const control = Dialog.useDialogControl() const onChangeInternal = React.useCallback( (date: Date | undefined) => { @@ -36,17 +45,43 @@ export function DateField({ ) return ( - - + - + + + + + + + + + + + + ) } diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx index 32f38a5d1..a3aa302f9 100644 --- a/src/components/forms/DateField/index.web.tsx +++ b/src/components/forms/DateField/index.web.tsx @@ -2,6 +2,7 @@ import React from 'react' import {TextInput, TextInputProps, StyleSheet} from 'react-native' // @ts-ignore import {unstable_createElement} from 'react-native-web' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' import * as TextField from '#/components/forms/TextField' import {toSimpleDateString} from '#/components/forms/DateField/utils' @@ -37,6 +38,7 @@ export function DateField({ label, isInvalid, testID, + accessibilityHint, }: DateFieldProps) { const handleOnChange = React.useCallback( (e: any) => { @@ -52,12 +54,14 @@ export function DateField({ return ( + {}} testID={testID} + accessibilityHint={accessibilityHint} /> ) diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts index 129f5672d..5400cf903 100644 --- a/src/components/forms/DateField/types.ts +++ b/src/components/forms/DateField/types.ts @@ -4,4 +4,5 @@ export type DateFieldProps = { label: string isInvalid?: boolean testID?: string + accessibilityHint?: string } diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 3cffe5b2b..376883c9d 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -126,8 +126,8 @@ export function useSharedInputStyles() { export type InputProps = Omit & { label: string - value: string - onChangeText: (value: string) => void + value?: string + onChangeText?: (value: string) => void isInvalid?: boolean inputRef?: React.RefObject } @@ -277,7 +277,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType}) { = 3, totalLength: fullHandle.length <= 253, } diff --git a/src/screens/Signup/StepCaptcha.tsx b/src/screens/Signup/StepCaptcha.tsx new file mode 100644 index 000000000..c4181e552 --- /dev/null +++ b/src/screens/Signup/StepCaptcha.tsx @@ -0,0 +1,94 @@ +import React from 'react' +import {ActivityIndicator, StyleSheet, View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {nanoid} from 'nanoid/non-secure' +import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state' +import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView' +import {createFullHandle} from 'lib/strings/handles' +import {isWeb} from 'platform/detection' +import {atoms as a, useTheme} from '#/alf' +import {FormError} from '#/components/forms/FormError' +import {ScreenTransition} from '#/screens/Login/ScreenTransition' + +const CAPTCHA_PATH = '/gate/signup' + +export function StepCaptcha() { + const {_} = useLingui() + const theme = useTheme() + const {state, dispatch} = useSignupContext() + const submit = useSubmitSignup({state, dispatch}) + + const [completed, setCompleted] = React.useState(false) + + const stateParam = React.useMemo(() => nanoid(15), []) + const url = React.useMemo(() => { + const newUrl = new URL(state.serviceUrl) + newUrl.pathname = CAPTCHA_PATH + newUrl.searchParams.set( + 'handle', + createFullHandle(state.handle, state.userDomain), + ) + newUrl.searchParams.set('state', stateParam) + newUrl.searchParams.set('colorScheme', theme.name) + + return newUrl.href + }, [state.serviceUrl, state.handle, state.userDomain, stateParam, theme.name]) + + const onSuccess = React.useCallback( + (code: string) => { + setCompleted(true) + submit(code) + }, + [submit], + ) + + const onError = React.useCallback(() => { + dispatch({ + type: 'setError', + value: _(msg`Error receiving captcha response.`), + }) + }, [_, dispatch]) + + return ( + + + + {!completed ? ( + + ) : ( + + )} + + + + + ) +} + +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/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx new file mode 100644 index 000000000..e0a79e8fb --- /dev/null +++ b/src/screens/Signup/StepHandle.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import {View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {useLingui} from '@lingui/react' +import {msg, Trans} from '@lingui/macro' +import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import * as TextField from '#/components/forms/TextField' +import {useSignupContext} from '#/screens/Signup/state' +import {Text} from '#/components/Typography' +import {atoms as a, useTheme} from '#/alf' +import { + createFullHandle, + IsValidHandle, + validateHandle, +} from 'lib/strings/handles' +import {ScreenTransition} from '#/screens/Login/ScreenTransition' + +export function StepHandle() { + const {_} = useLingui() + const t = useTheme() + const {state, dispatch} = useSignupContext() + + const [validCheck, setValidCheck] = React.useState({ + handleChars: false, + hyphenStartOrEnd: false, + frontLength: false, + totalLength: true, + overall: false, + }) + + useFocusEffect( + React.useCallback(() => { + console.log('run') + setValidCheck(validateHandle(state.handle, state.userDomain)) + }, [state.handle, state.userDomain]), + ) + + const onHandleChange = React.useCallback( + (value: string) => { + if (state.error) { + dispatch({type: 'setError', value: ''}) + } + + dispatch({ + type: 'setHandle', + value, + }) + }, + [dispatch, state.error], + ) + + return ( + + + + + + + + + + Your full handle will be{' '} + + @{createFullHandle(state.handle, state.userDomain)} + + + + + {state.error ? ( + + + {state.error} + + ) : undefined} + {validCheck.hyphenStartOrEnd ? ( + + + + Only contains letters, numbers, and hyphens + + + ) : ( + + + + Doesn't begin or end with a hyphen + + + )} + + + {!validCheck.totalLength ? ( + + No longer than 253 characters + + ) : ( + + At least 3 characters + + )} + + + + + ) +} + +function IsValidIcon({valid}: {valid: boolean}) { + const t = useTheme() + if (!valid) { + return + } + return +} diff --git a/src/screens/Signup/StepInfo.tsx b/src/screens/Signup/StepInfo.tsx new file mode 100644 index 000000000..30a31884a --- /dev/null +++ b/src/screens/Signup/StepInfo.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {atoms as a} from '#/alf' +import * as TextField from '#/components/forms/TextField' +import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' +import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' +import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' +import {is13, is18, useSignupContext} from '#/screens/Signup/state' +import * as DateField from '#/components/forms/DateField' +import {logger} from '#/logger' +import {Loader} from '#/components/Loader' +import {Policies} from 'view/com/auth/create/Policies' +import {HostingProvider} from '#/components/forms/HostingProvider' +import {FormError} from '#/components/forms/FormError' +import {ScreenTransition} from '#/screens/Login/ScreenTransition' + +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 StepInfo() { + const {_} = useLingui() + const {state, dispatch} = useSignupContext() + + return ( + + + + + + Hosting provider + + + dispatch({type: 'setServiceUrl', value: v}) + } + /> + + {state.isLoading ? ( + + + + ) : state.serviceDescription ? ( + <> + {state.serviceDescription.inviteCodeRequired && ( + + + Invite code + + + + { + dispatch({ + type: 'setInviteCode', + value: value.trim(), + }) + }} + label={_(msg`Required for this provider`)} + defaultValue={state.inviteCode} + autoCapitalize="none" + autoComplete="email" + keyboardType="email-address" + /> + + + )} + + + Email + + + + { + dispatch({ + type: 'setEmail', + value: value.trim(), + }) + }} + label={_(msg`Enter your email address`)} + defaultValue={state.email} + autoCapitalize="none" + autoComplete="email" + keyboardType="email-address" + /> + + + + + Password + + + + { + dispatch({ + type: 'setPassword', + value, + }) + }} + label={_(msg`Choose your password`)} + defaultValue={state.password} + secureTextEntry + autoComplete="new-password" + /> + + + + + Your birth date + + { + dispatch({ + type: 'setDateOfBirth', + value: sanitizeDate(new Date(date)), + }) + }} + label={_(msg`Date of birth`)} + accessibilityHint={_(msg`Select your date of birth`)} + /> + + + + ) : undefined} + + + ) +} diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx new file mode 100644 index 000000000..b1acbbdf0 --- /dev/null +++ b/src/screens/Signup/index.tsx @@ -0,0 +1,225 @@ +import React from 'react' +import {ScrollView, View} from 'react-native' +import {useLingui} from '@lingui/react' +import {msg, Trans} from '@lingui/macro' +import { + initialState, + reducer, + SignupContext, + SignupStep, + useSubmitSignup, +} from '#/screens/Signup/state' +import {StepInfo} from '#/screens/Signup/StepInfo' +import {StepHandle} from '#/screens/Signup/StepHandle' +import {StepCaptcha} from '#/screens/Signup/StepCaptcha' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' +import {FEEDBACK_FORM_URL} from 'lib/constants' +import {InlineLink} from '#/components/Link' +import {useServiceQuery} from 'state/queries/service' +import {getAgent} from 'state/session' +import {createFullHandle} from 'lib/strings/handles' +import {useAnalytics} from 'lib/analytics/analytics' + +export function Signup({onPressBack}: {onPressBack: () => void}) { + const {_} = useLingui() + const t = useTheme() + const {screen} = useAnalytics() + const [state, dispatch] = React.useReducer(reducer, initialState) + const submit = useSubmitSignup({state, dispatch}) + + const { + data: serviceInfo, + isFetching, + isError, + refetch, + } = useServiceQuery(state.serviceUrl) + + React.useEffect(() => { + screen('CreateAccount') + }, [screen]) + + React.useEffect(() => { + if (isFetching) { + dispatch({type: 'setIsLoading', value: true}) + } else if (!isFetching) { + dispatch({type: 'setIsLoading', value: false}) + } + }, [isFetching]) + + React.useEffect(() => { + if (isError) { + dispatch({type: 'setServiceDescription', value: undefined}) + dispatch({ + type: 'setError', + value: _( + msg`Unable to contact your service. Please check your Internet connection.`, + ), + }) + } else if (serviceInfo) { + dispatch({type: 'setServiceDescription', value: serviceInfo}) + dispatch({type: 'setError', value: ''}) + } + }, [_, serviceInfo, isError]) + + const onNextPress = React.useCallback(async () => { + if (state.activeStep === SignupStep.HANDLE) { + try { + dispatch({type: 'setIsLoading', value: true}) + + const res = await getAgent().resolveHandle({ + handle: createFullHandle(state.handle, state.userDomain), + }) + + if (res.data.did) { + dispatch({ + type: 'setError', + value: _(msg`That handle is already taken.`), + }) + return + } + } catch (e) { + // Don't have to handle + } finally { + dispatch({type: 'setIsLoading', value: false}) + } + } + + // phoneVerificationRequired is actually whether a captcha is required + if ( + state.activeStep === SignupStep.HANDLE && + !state.serviceDescription?.phoneVerificationRequired + ) { + submit() + return + } + + dispatch({type: 'next'}) + }, [ + _, + state.activeStep, + state.handle, + state.serviceDescription?.phoneVerificationRequired, + state.userDomain, + submit, + ]) + + const onBackPress = React.useCallback(() => { + if (state.activeStep !== SignupStep.INFO) { + dispatch({type: 'prev'}) + } else { + onPressBack() + } + }, [onPressBack, state.activeStep]) + + return ( + + + + + + + Step {state.activeStep + 1} of{' '} + {state.serviceDescription && + !state.serviceDescription.phoneVerificationRequired + ? '2' + : '3'} + + + {state.activeStep === SignupStep.INFO ? ( + Your account + ) : state.activeStep === SignupStep.HANDLE ? ( + Your user handle + ) : ( + Complete the challenge + )} + + + + {state.activeStep === SignupStep.INFO ? ( + + ) : state.activeStep === SignupStep.HANDLE ? ( + + ) : ( + + )} + + + + + {state.activeStep !== SignupStep.CAPTCHA && ( + <> + {isError ? ( + + ) : ( + + )} + + )} + + + + Having trouble?{' '} + + Contact support + + + + + + + + ) +} diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts new file mode 100644 index 000000000..1ae43612e --- /dev/null +++ b/src/screens/Signup/state.ts @@ -0,0 +1,320 @@ +import React, {useCallback} from 'react' +import {LayoutAnimation} from 'react-native' +import * as EmailValidator from 'email-validator' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {cleanError} from 'lib/strings/errors' +import { + ComAtprotoServerCreateAccount, + ComAtprotoServerDescribeServer, +} from '@atproto/api' + +import {logger} from '#/logger' +import {DEFAULT_SERVICE, IS_PROD_SERVICE} from 'lib/constants' +import {createFullHandle, validateHandle} from 'lib/strings/handles' +import {getAge} from 'lib/strings/time' +import {useSessionApi} from 'state/session' +import { + DEFAULT_PROD_FEEDS, + usePreferencesSetBirthDateMutation, + useSetSaveFeedsMutation, +} from 'state/queries/preferences' +import {useOnboardingDispatch} from 'state/shell' + +export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema + +const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago + +export enum SignupStep { + INFO, + HANDLE, + CAPTCHA, +} + +export type SignupState = { + hasPrev: boolean + canNext: boolean + activeStep: SignupStep + + serviceUrl: string + serviceDescription?: ServiceDescription + userDomain: string + dateOfBirth: Date + email: string + password: string + inviteCode: string + handle: string + + error: string + isLoading: boolean +} + +export type SignupAction = + | {type: 'prev'} + | {type: 'next'} + | {type: 'finish'} + | {type: 'setStep'; value: SignupStep} + | {type: 'setServiceUrl'; value: string} + | {type: 'setServiceDescription'; value: ServiceDescription | undefined} + | {type: 'setEmail'; value: string} + | {type: 'setPassword'; value: string} + | {type: 'setDateOfBirth'; value: Date} + | {type: 'setInviteCode'; value: string} + | {type: 'setHandle'; value: string} + | {type: 'setVerificationCode'; value: string} + | {type: 'setError'; value: string} + | {type: 'setCanNext'; value: boolean} + | {type: 'setIsLoading'; value: boolean} + +export const initialState: SignupState = { + hasPrev: false, + canNext: false, + activeStep: SignupStep.INFO, + + serviceUrl: DEFAULT_SERVICE, + serviceDescription: undefined, + userDomain: '', + dateOfBirth: DEFAULT_DATE, + email: '', + password: '', + handle: '', + inviteCode: '', + + error: '', + isLoading: false, +} + +export function is13(date: Date) { + return getAge(date) >= 13 +} + +export function is18(date: Date) { + return getAge(date) >= 18 +} + +export function reducer(s: SignupState, a: SignupAction): SignupState { + let next = {...s} + + switch (a.type) { + case 'prev': { + if (s.activeStep !== SignupStep.INFO) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + next.activeStep-- + next.error = '' + } + break + } + case 'next': { + if (s.activeStep !== SignupStep.CAPTCHA) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + next.activeStep++ + next.error = '' + } + break + } + case 'setStep': { + next.activeStep = a.value + break + } + case 'setServiceUrl': { + next.serviceUrl = a.value + break + } + case 'setServiceDescription': { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + + next.serviceDescription = a.value + next.userDomain = a.value?.availableUserDomains[0] ?? '' + next.isLoading = false + break + } + + case 'setEmail': { + next.email = a.value + break + } + case 'setPassword': { + next.password = a.value + break + } + case 'setDateOfBirth': { + next.dateOfBirth = a.value + break + } + case 'setInviteCode': { + next.inviteCode = a.value + break + } + case 'setHandle': { + next.handle = a.value + break + } + case 'setCanNext': { + next.canNext = a.value + break + } + case 'setIsLoading': { + next.isLoading = a.value + break + } + case 'setError': { + next.error = a.value + break + } + } + + next.hasPrev = next.activeStep !== SignupStep.INFO + + switch (next.activeStep) { + case SignupStep.INFO: { + const isValidEmail = EmailValidator.validate(next.email) + next.canNext = + !!(next.email && next.password && next.dateOfBirth) && + (!next.serviceDescription?.inviteCodeRequired || !!next.inviteCode) && + is13(next.dateOfBirth) && + isValidEmail + break + } + case SignupStep.HANDLE: { + next.canNext = + !!next.handle && validateHandle(next.handle, next.userDomain).overall + break + } + } + + logger.debug('signup', next) + + if (s.activeStep !== next.activeStep) { + logger.debug('signup: step changed', {activeStep: next.activeStep}) + } + + return next +} + +interface IContext { + state: SignupState + dispatch: React.Dispatch +} +export const SignupContext = React.createContext({} as IContext) +export const useSignupContext = () => React.useContext(SignupContext) + +export function useSubmitSignup({ + state, + dispatch, +}: { + state: SignupState + dispatch: (action: SignupAction) => void +}) { + const {_} = useLingui() + const {createAccount} = useSessionApi() + const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() + const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() + const onboardingDispatch = useOnboardingDispatch() + + return useCallback( + async (verificationCode?: string) => { + if (!state.email) { + dispatch({type: 'setStep', value: SignupStep.INFO}) + return dispatch({ + type: 'setError', + value: _(msg`Please enter your email.`), + }) + } + if (!EmailValidator.validate(state.email)) { + dispatch({type: 'setStep', value: SignupStep.INFO}) + return dispatch({ + type: 'setError', + value: _(msg`Your email appears to be invalid.`), + }) + } + if (!state.password) { + dispatch({type: 'setStep', value: SignupStep.INFO}) + return dispatch({ + type: 'setError', + value: _(msg`Please choose your password.`), + }) + } + if (!state.handle) { + dispatch({type: 'setStep', value: SignupStep.HANDLE}) + return dispatch({ + type: 'setError', + value: _(msg`Please choose your handle.`), + }) + } + if ( + state.serviceDescription?.phoneVerificationRequired && + !verificationCode + ) { + dispatch({type: 'setStep', value: SignupStep.CAPTCHA}) + return dispatch({ + type: 'setError', + value: _(msg`Please complete the verification captcha.`), + }) + } + dispatch({type: 'setError', value: ''}) + dispatch({type: 'setIsLoading', value: true}) + + try { + onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view + await createAccount({ + service: state.serviceUrl, + email: state.email, + handle: createFullHandle(state.handle, state.userDomain), + password: state.password, + inviteCode: state.inviteCode.trim(), + verificationCode: verificationCode, + }) + setBirthDate({birthDate: state.dateOfBirth}) + if (IS_PROD_SERVICE(state.serviceUrl)) { + setSavedFeeds(DEFAULT_PROD_FEEDS) + } + } catch (e: any) { + onboardingDispatch({type: 'skip'}) // undo starting the onboard + let errMsg = e.toString() + if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { + dispatch({ + type: 'setError', + value: _( + msg`Invite code not accepted. Check that you input it correctly and try again.`, + ), + }) + dispatch({type: 'setStep', value: SignupStep.INFO}) + return + } + + if ([400, 429].includes(e.status)) { + logger.warn('Failed to create account', {message: e}) + } else { + logger.error(`Failed to create account (${e.status} status)`, { + message: e, + }) + } + + const error = cleanError(errMsg) + const isHandleError = error.toLowerCase().includes('handle') + + dispatch({type: 'setIsLoading', value: false}) + dispatch({type: 'setError', value: cleanError(errMsg)}) + dispatch({type: 'setStep', value: isHandleError ? 2 : 1}) + } finally { + dispatch({type: 'setIsLoading', value: false}) + } + }, + [ + state.email, + state.password, + state.handle, + state.serviceDescription?.phoneVerificationRequired, + state.serviceUrl, + state.userDomain, + state.inviteCode, + state.dateOfBirth, + dispatch, + _, + onboardingDispatch, + createAccount, + setBirthDate, + setSavedFeeds, + ], + ) +} diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 58604ec9e..b22bbb7fe 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -7,7 +7,7 @@ import {useNavigation} from '@react-navigation/native' import {isIOS, isNative} from '#/platform/detection' import {Login} from '#/screens/Login' -import {CreateAccount} from '#/view/com/auth/create/CreateAccount' +import {Signup} from '#/screens/Signup' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {s} from '#/lib/styles' import {usePalette} from '#/lib/hooks/usePalette' @@ -148,7 +148,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { /> ) : undefined} {screenState === ScreenState.S_CreateAccount ? ( - 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 ( - - - - {uiState.step === 1 && ( - - )} - {uiState.step === 2 && ( - - )} - {uiState.step === 3 && ( - - )} - - - - - Back - - - - {uiState.canNext ? ( - - {uiState.isProcessing ? ( - - ) : ( - - Next - - )} - - ) : serviceInfoError ? ( - refetchServiceInfo()} - accessibilityRole="button" - accessibilityLabel={_(msg`Retry`)} - accessibilityHint="" - accessibilityLiveRegion="polite" - hitSlop={HITSLOP_10}> - - Retry - - - ) : serviceInfoIsFetching ? ( - <> - - - Connecting... - - - ) : undefined} - - - - - - Having trouble?{' '} - - - - - - - - - ) -} - -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 2c7d60818..dc3c9c174 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, @@ -15,9 +15,11 @@ type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema export const Policies = ({ serviceDescription, needsGuardian, + under13, }: { serviceDescription: ServiceDescription needsGuardian: boolean + under13: boolean }) => { const pal = usePalette('default') if (!serviceDescription) { @@ -53,6 +55,7 @@ export const Policies = ({ href={tos} text="Terms of Service" style={[pal.link, s.underline]} + onPress={() => Linking.openURL(tos)} />, ) } @@ -63,6 +66,7 @@ export const Policies = ({ href={pp} text="Privacy Policy" style={[pal.link, s.underline]} + onPress={() => Linking.openURL(pp)} />, ) } @@ -81,12 +85,16 @@ export const Policies = ({ By creating an account you agree to the {els}. - {needsGuardian && ( + {under13 ? ( + + You must be 13 years of age or older to sign up. + + ) : needsGuardian ? ( 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. - )} + ) : undefined} ) } 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 ( - - uiDispatch({type: 'set-service-url', value: url})} - /> - - - {uiState.error ? ( - - ) : undefined} - - - - Hosting provider - - - - - - - {toNiceDomain(uiState.serviceUrl)} - - - - - - - - - - {!uiState.serviceDescription ? ( - - ) : ( - <> - {uiState.isInviteCodeRequired && ( - - - Invite code - - 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} - /> - - )} - - {!uiState.isInviteCodeRequired || uiState.inviteCode ? ( - <> - - - Email address - - 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} - /> - - - - - Password - - uiDispatch({type: 'set-password', value})} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={_(msg`Set password`)} - accessibilityLabelledBy="password" - autoCapitalize="none" - autoComplete="new-password" - autoCorrect={false} - /> - - - - - Your birth date - - - 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" - /> - - - {uiState.serviceDescription && ( - - )} - - ) : undefined} - - )} - - ) -} - -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({ - 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 ( - - - - - - - Your full handle will be{' '} - - @{createFullHandle(uiState.handle, uiState.userDomain)} - - - - - {uiState.error ? ( - - - - {uiState.error} - - - ) : undefined} - - - - May only contain letters and numbers - - - - - {!validCheck.totalLength ? ( - - May not be longer than 253 characters - - ) : ( - - Must be at least 3 characters - - )} - - - - - ) -} - -function IsValidIcon({valid}: {valid: boolean}) { - const t = useTheme() - - if (!valid) { - return - } - - return -} 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 ( - - - - {!completed ? ( - - ) : ( - - )} - - - {uiState.error ? ( - - ) : undefined} - - ) -} - -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 ( - - - - {uiState.step === 3 ? ( - Last step! - ) : ( - - Step {uiState.step} of {numSteps} - - )} - - - - {title} - - - {children} - - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 20, - }, -}) -- cgit 1.4.1 From 10943b4ff469ab540a7f28647688a3fd3001d5b4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 19:52:27 +0000 Subject: fix merge issue (double hook) --- src/components/Lists.tsx | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 8e4a58007..d3e072028 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -150,7 +150,6 @@ export function ListMaybePlaceholder({ const t = useTheme() const {_} = useLingui() const {gtMobile, gtTablet} = useBreakpoints() - const {_} = useLingui() if (!isLoading && isError) { return ( -- cgit 1.4.1 From b226f41560f2e78d9148ec6e034fb0c00e25422a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 20:56:13 +0000 Subject: use in-button spinner instead of activity indicator --- src/screens/Login/LoginForm.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index ee47aa41d..eddcc9678 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -16,7 +16,7 @@ import {isNetworkError} from 'lib/strings/errors' import {useSessionApi} from '#/state/session' import {cleanError} from 'lib/strings/errors' import {logger} from '#/logger' -import {Button, ButtonText} from '#/components/Button' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' import * as TextField from '#/components/forms/TextField' @@ -25,6 +25,7 @@ import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' import {HostingProvider} from '#/components/forms/HostingProvider' import {FormContainer} from './FormContainer' import {FormError} from '#/components/forms/FormError' +import {Loader} from '#/components/Loader' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -64,6 +65,7 @@ export const LoginForm = ({ }, [track]) const onPressNext = async () => { + if (isProcessing) return Keyboard.dismiss() setError('') setIsProcessing(true) @@ -237,8 +239,6 @@ export const LoginForm = ({ Connecting... - ) : isProcessing ? ( - ) : isReady ? ( ) : undefined} -- cgit 1.4.1 From 8b5279ce15820b48b3557e49904bb736ca53e4d3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 21:10:31 +0000 Subject: animate login spinner --- package.json | 3 ++- src/screens/Login/LoginForm.tsx | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/package.json b/package.json index 97fd91b03..b664dd22a 100644 --- a/package.json +++ b/package.json @@ -312,5 +312,6 @@ }, "lint-staged": { "*{.js,.jsx,.ts,.tsx}": "yarn eslint --fix" - } + }, + "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" } diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index eddcc9678..e9620db5c 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -2,6 +2,7 @@ import React, {useState, useRef} from 'react' import { ActivityIndicator, Keyboard, + LayoutAnimation, TextInput, TouchableOpacity, View, @@ -67,6 +68,7 @@ export const LoginForm = ({ const onPressNext = async () => { if (isProcessing) return Keyboard.dismiss() + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setError('') setIsProcessing(true) @@ -101,6 +103,7 @@ export const LoginForm = ({ }) } catch (e: any) { const errMsg = e.toString() + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setIsProcessing(false) if (errMsg.includes('Authentication Required')) { logger.debug('Failed to login due to invalid credentials', { -- cgit 1.4.1 From fd448a5fab2e5239abcf4504a1de24cb7aca700c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 21:28:18 +0000 Subject: rename pencil -> pencilLine and reexport --- assets/icons/pencilLine_stroke2_corner0_rounded.svg | 1 + assets/icons/pencil_stroke2_corner0_rounded.svg | 1 - src/components/forms/HostingProvider.tsx | 2 +- src/components/icons/Pencil.tsx | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 assets/icons/pencilLine_stroke2_corner0_rounded.svg delete mode 100644 assets/icons/pencil_stroke2_corner0_rounded.svg (limited to 'src') diff --git a/assets/icons/pencilLine_stroke2_corner0_rounded.svg b/assets/icons/pencilLine_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..3ff2161d9 --- /dev/null +++ b/assets/icons/pencilLine_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/pencil_stroke2_corner0_rounded.svg b/assets/icons/pencil_stroke2_corner0_rounded.svg deleted file mode 100644 index 734198989..000000000 --- a/assets/icons/pencil_stroke2_corner0_rounded.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx index df506b77c..c8cd7263f 100644 --- a/src/components/forms/HostingProvider.tsx +++ b/src/components/forms/HostingProvider.tsx @@ -4,7 +4,7 @@ import {TouchableOpacity, View} from 'react-native' import {isAndroid} from '#/platform/detection' import {atoms as a, useTheme} from '#/alf' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' -import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import {PencilLine_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' import * as TextField from './TextField' import {useDialogControl} from '../Dialog' import {Text} from '../Typography' diff --git a/src/components/icons/Pencil.tsx b/src/components/icons/Pencil.tsx index 1b7fc17cf..854d51a3b 100644 --- a/src/components/icons/Pencil.tsx +++ b/src/components/icons/Pencil.tsx @@ -1,5 +1,5 @@ import {createSinglePathSVG} from './TEMPLATE' -export const Pencil_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M13.586 1.5a2 2 0 0 1 2.828 0L19.5 4.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 5.086 21H1a1 1 0 0 1-1-1v-4.086A2 2 0 0 1 .586 14.5l13-13ZM15 2.914l-13 13V19h3.086l13-13L15 2.914ZM11 20a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', +export const PencilLine_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', }) -- cgit 1.4.1 From 717c53bea82cfe902af9d1ea17d43b4d8064de04 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 21:30:41 +0000 Subject: remove explicit height/width --- src/components/forms/HostingProvider.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'src') diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx index c8cd7263f..c0dd7218e 100644 --- a/src/components/forms/HostingProvider.tsx +++ b/src/components/forms/HostingProvider.tsx @@ -57,11 +57,7 @@ export function HostingProvider({ t.atoms.bg_contrast_100, {marginLeft: 'auto', left: 6, padding: 6}, ]}> - + -- cgit 1.4.1 From 8556016a69642477e497fb7d695521b01945febc Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 21:37:12 +0000 Subject: remove textfield references from hosting provider --- src/components/forms/HostingProvider.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx index c0dd7218e..80660b580 100644 --- a/src/components/forms/HostingProvider.tsx +++ b/src/components/forms/HostingProvider.tsx @@ -5,7 +5,6 @@ import {isAndroid} from '#/platform/detection' import {atoms as a, useTheme} from '#/alf' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {PencilLine_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' -import * as TextField from './TextField' import {useDialogControl} from '../Dialog' import {Text} from '../Typography' import {ServerInputDialog} from '#/view/com/auth/server-input' @@ -49,7 +48,9 @@ export function HostingProvider({ t.atoms.bg_contrast_25, ]} onPress={onPressSelectService}> - + + + {toNiceDomain(serviceUrl)} - + -- cgit 1.4.1 From 2428d22368a96450f9a99b79a7e689a57e899a48 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 21:58:53 +0000 Subject: change hosting provider and forgot pw touchables to button --- src/components/forms/HostingProvider.tsx | 54 ++++++++++++++++++++++---------- src/screens/Login/LoginForm.tsx | 12 +++---- 2 files changed, 43 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx index 80660b580..1653b0da4 100644 --- a/src/components/forms/HostingProvider.tsx +++ b/src/components/forms/HostingProvider.tsx @@ -1,5 +1,7 @@ import React from 'react' -import {TouchableOpacity, View} from 'react-native' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {isAndroid} from '#/platform/detection' import {atoms as a, useTheme} from '#/alf' @@ -9,6 +11,7 @@ import {useDialogControl} from '../Dialog' import {Text} from '../Typography' import {ServerInputDialog} from '#/view/com/auth/server-input' import {toNiceDomain} from '#/lib/strings/url-helpers' +import {Button} from '../Button' export function HostingProvider({ serviceUrl, @@ -21,6 +24,7 @@ export function HostingProvider({ }) { const serverInputControl = useDialogControl() const t = useTheme() + const {_} = useLingui() const onPressSelectService = React.useCallback(() => { serverInputControl.open() @@ -35,8 +39,11 @@ export function HostingProvider({ control={serverInputControl} onSelect={onSelectServiceUrl} /> - - - - - {toNiceDomain(serviceUrl)} - - - - + {({hovered}) => ( + <> + + + + {toNiceDomain(serviceUrl)} + + + + + )} + ) } diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index e9620db5c..8c9ef3e27 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -4,7 +4,6 @@ import { Keyboard, LayoutAnimation, TextInput, - TouchableOpacity, View, } from 'react-native' import {ComAtprotoServerDescribeServer} from '@atproto/api' @@ -193,22 +192,23 @@ export const LoginForm = ({ : _(msg`Input the password tied to ${identifier}`) } /> - - + Forgot? - + -- cgit 1.4.1 From 1f02ed5d871a4b6675f3c344a4cae2aea26896b3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 22:03:49 +0000 Subject: change choose account form to use Button + add hover styles --- src/screens/Login/ChooseAccountForm.tsx | 90 ++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index 7a3a4555b..4fcca5f52 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {TouchableOpacity, View} from 'react-native' +import {View} from 'react-native' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import flattenReactChildren from 'react-keyed-flatten-children' @@ -67,31 +67,42 @@ function AccountItem({ }, [account, onSelect]) return ( - - - - - - - - {profile?.displayName || account.handle}{' '} + label={ + isCurrentAccount + ? _(msg`Continue as ${account.handle} (currently signed in)`) + : _(msg`Sign in as ${account.handle}`) + }> + {({hovered}) => ( + + + + + + + {profile?.displayName || account.handle}{' '} + + {account.handle} - {account.handle} - - {isCurrentAccount ? ( - - ) : ( - - )} - - + {isCurrentAccount ? ( + + ) : ( + + )} + + )} + ) } export const ChooseAccountForm = ({ @@ -149,28 +160,35 @@ export const ChooseAccountForm = ({ isCurrentAccount={account.did === currentAccount?.did} /> ))} - onSelectAccount(undefined)} - accessibilityRole="button" - accessibilityLabel={_(msg`Login to account that is not listed`)} - accessibilityHint=""> - - + {({hovered}) => ( + - Other account - - - - + + Other account + + + + )} + -- cgit 1.4.1 From 49cd7e4bcf599c17c3e466cc9f483ec063e926c1 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 19 Mar 2024 22:06:59 +0000 Subject: add pressed state and rm cloneElement --- src/screens/Login/ChooseAccountForm.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index 4fcca5f52..dd807ba3a 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -35,13 +35,7 @@ function Group({children}: {children: React.ReactNode}) { {i > 0 ? ( ) : null} - {React.cloneElement(child, { - // @ts-ignore - style: { - borderRadius: 0, - borderWidth: 0, - }, - })} + {child} ) : null })} @@ -77,14 +71,14 @@ function AccountItem({ ? _(msg`Continue as ${account.handle} (currently signed in)`) : _(msg`Sign in as ${account.handle}`) }> - {({hovered}) => ( + {({hovered, pressed}) => ( @@ -165,7 +159,7 @@ export const ChooseAccountForm = ({ style={[a.flex_1]} onPress={() => onSelectAccount(undefined)} label={_(msg`Login to account that is not listed`)}> - {({hovered}) => ( + {({hovered, pressed}) => ( Date: Tue, 19 Mar 2024 22:16:29 +0000 Subject: hog FormError --- src/components/forms/FormError.tsx | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx index 3c6a8649d..05f2e5893 100644 --- a/src/components/forms/FormError.tsx +++ b/src/components/forms/FormError.tsx @@ -1,10 +1,9 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' +import {View} from 'react-native' import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' import {Text} from '#/components/Typography' import {atoms as a, useTheme} from '#/alf' -import {colors} from '#/lib/styles' export function FormError({error}: {error?: string}) { const t = useTheme() @@ -12,7 +11,15 @@ export function FormError({error}: {error?: string}) { if (!error) return null return ( - + {error} @@ -20,15 +27,3 @@ export function FormError({error}: {error?: string}) { ) } - -const styles = StyleSheet.create({ - error: { - backgroundColor: colors.red4, - flexDirection: 'row', - alignItems: 'center', - marginBottom: 15, - borderRadius: 8, - paddingHorizontal: 8, - paddingVertical: 8, - }, -}) -- cgit 1.4.1 From 09e9769eeaa89e09e3b63c48533abc46f193cbfd Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 20 Mar 2024 15:40:03 +0000 Subject: add logEvent that got lost in merge from main --- src/screens/Login/ChooseAccountForm.tsx | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index dd807ba3a..6e32d472e 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -18,6 +18,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icon import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import * as TextField from '#/components/forms/TextField' import {FormContainer} from './FormContainer' +import {logEvent} from '#/lib/statsig/statsig' function Group({children}: {children: React.ReactNode}) { const t = useTheme() @@ -125,6 +126,10 @@ export const ChooseAccountForm = ({ Toast.show(_(msg`Already signed in as @${account.handle}`)) } else { await initSession(account) + logEvent('account:loggedIn', { + logContext: 'ChooseAccountForm', + withPassword: false, + }) track('Sign In', {resumedSession: true}) setTimeout(() => { Toast.show(_(msg`Signed in as @${account.handle}`)) -- cgit 1.4.1 From 4c60d4d07014c4970dfdb7f6272ab2abadf035b9 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 20 Mar 2024 15:42:39 +0000 Subject: remove unnecessary group component --- src/screens/Login/ChooseAccountForm.tsx | 50 +++++++++++---------------------- 1 file changed, 17 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index 6e32d472e..600a640ca 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -2,7 +2,6 @@ import React from 'react' import {View} from 'react-native' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import flattenReactChildren from 'react-keyed-flatten-children' import {useAnalytics} from 'lib/analytics/analytics' import {UserAvatar} from '../../view/com/util/UserAvatar' @@ -20,30 +19,6 @@ import * as TextField from '#/components/forms/TextField' import {FormContainer} from './FormContainer' import {logEvent} from '#/lib/statsig/statsig' -function Group({children}: {children: React.ReactNode}) { - const t = useTheme() - return ( - - {flattenReactChildren(children).map((child, i) => { - return React.isValidElement(child) ? ( - - {i > 0 ? ( - - ) : null} - {child} - - ) : null - })} - - ) -} - function AccountItem({ account, onSelect, @@ -150,14 +125,23 @@ export const ChooseAccountForm = ({ Sign in as... - + {accounts.map(account => ( - + <> + + + ))} - + ) -- cgit 1.4.1 From aa239451afe75a38e7623bf829554474ba0db138 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 20 Mar 2024 16:26:30 -0500 Subject: Signup shell tweaks --- src/alf/atoms.ts | 7 ++++--- src/alf/tokens.ts | 8 ++++---- src/screens/Signup/index.tsx | 49 +++++++++++++++----------------------------- 3 files changed, 25 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index ef285c09a..45ab72ca6 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -1,6 +1,7 @@ import {Platform} from 'react-native' -import {web, native} from '#/alf/util/platform' + import * as tokens from '#/alf/tokens' +import {native, web} from '#/alf/util/platform' export const atoms = { /* @@ -253,10 +254,10 @@ export const atoms = { fontWeight: tokens.fontWeight.normal, }, font_semibold: { - fontWeight: '500', + fontWeight: tokens.fontWeight.semibold, }, font_bold: { - fontWeight: tokens.fontWeight.semibold, + fontWeight: tokens.fontWeight.bold, }, italic: { fontStyle: 'italic', diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 4045c831c..1bddd95d4 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,8 +1,8 @@ import { BLUE_HUE, - RED_HUE, - GREEN_HUE, generateScale, + GREEN_HUE, + RED_HUE, } from '#/alf/util/colorGeneration' export const scale = generateScale(6, 100) @@ -116,8 +116,8 @@ export const borderRadius = { export const fontWeight = { normal: '400', - semibold: '600', - bold: '900', + semibold: '500', + bold: '600', } as const export const gradients = { diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index 3414aacc1..f19823b4f 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -21,6 +21,7 @@ import {StepHandle} from '#/screens/Signup/StepHandle' import {StepInfo} from '#/screens/Signup/StepInfo' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' +import {Divider} from '#/components/Divider' import {InlineLink} from '#/components/Link' import {Text} from '#/components/Typography' @@ -126,16 +127,9 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { keyboardShouldPersistTaps="handled" style={a.h_full} keyboardDismissMode="on-drag"> - - - + + + Step {state.activeStep + 1} of{' '} {state.serviceDescription && !state.serviceDescription.phoneVerificationRequired @@ -152,7 +146,8 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { )} - + + {state.activeStep === SignupStep.INFO ? ( ) : state.activeStep === SignupStep.HANDLE ? ( @@ -162,12 +157,12 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { )} - + @@ -178,7 +173,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { label="Retry" variant="solid" color="primary" - size="small" + size="medium" disabled={state.isLoading} onPress={() => refetch()}> Retry @@ -187,12 +182,8 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { - + + + + { + passwordInputRef.current?.focus() + }} + blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field + value={identifier} + onChangeText={str => + setIdentifier((str || '').toLowerCase().trim()) + } + editable={!isProcessing} + accessibilityHint={_( + msg`Input the username or email address you used at signup`, + )} + /> + + + + + + + + - + @@ -251,7 +252,7 @@ export const LoginForm = ({ accessibilityHint={_(msg`Navigates to the next screen`)} variant="solid" color="primary" - size="small" + size="medium" onPress={onPressNext}> Next -- cgit 1.4.1 From b0fcfa563d85dd8835157089c39e300713bcfb7f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 20 Mar 2024 17:14:13 -0500 Subject: Password flow tweaks --- src/components/forms/FormError.tsx | 15 ++++++++------- src/screens/Login/ForgotPasswordForm.tsx | 27 ++++++++++++++------------- src/screens/Login/FormContainer.tsx | 2 +- src/screens/Login/SetNewPasswordForm.tsx | 10 ++++++---- 4 files changed, 29 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx index 905aeebd6..9e72df879 100644 --- a/src/components/forms/FormError.tsx +++ b/src/components/forms/FormError.tsx @@ -13,16 +13,17 @@ export function FormError({error}: {error?: string}) { return ( - - - {error} + + + + {error} + ) diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx index 37d24bb10..580452e75 100644 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -114,21 +114,22 @@ export const ForgotPasswordForm = ({ /> - - - - Enter the email you used to create your account. We'll send you a - "reset code" so you can set a new password. - - - + + + + Enter the email you used to create your account. We'll send you a + "reset code" so you can set a new password. + + + - + + -- cgit 1.4.1 From 19fab671a3b11daa73a169b99752a4d2ba9e0166 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 20 Mar 2024 17:25:08 -0500 Subject: Move some things around --- src/screens/Signup/StepCaptcha.tsx | 95 ------- src/screens/Signup/StepCaptcha/CaptchaWebView.tsx | 87 ++++++ .../Signup/StepCaptcha/CaptchaWebView.web.tsx | 61 +++++ src/screens/Signup/StepCaptcha/index.tsx | 95 +++++++ src/screens/Signup/StepInfo.tsx | 146 ---------- src/screens/Signup/StepInfo/Policies.tsx | 97 +++++++ src/screens/Signup/StepInfo/index.tsx | 146 ++++++++++ src/view/com/auth/create/CaptchaWebView.tsx | 86 ------ src/view/com/auth/create/CaptchaWebView.web.tsx | 61 ----- src/view/com/auth/create/Policies.tsx | 97 ------- src/view/com/auth/create/state.ts | 298 --------------------- 11 files changed, 486 insertions(+), 783 deletions(-) delete mode 100644 src/screens/Signup/StepCaptcha.tsx create mode 100644 src/screens/Signup/StepCaptcha/CaptchaWebView.tsx create mode 100644 src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx create mode 100644 src/screens/Signup/StepCaptcha/index.tsx delete mode 100644 src/screens/Signup/StepInfo.tsx create mode 100644 src/screens/Signup/StepInfo/Policies.tsx create mode 100644 src/screens/Signup/StepInfo/index.tsx delete mode 100644 src/view/com/auth/create/CaptchaWebView.tsx delete mode 100644 src/view/com/auth/create/CaptchaWebView.web.tsx delete mode 100644 src/view/com/auth/create/Policies.tsx delete mode 100644 src/view/com/auth/create/state.ts (limited to 'src') diff --git a/src/screens/Signup/StepCaptcha.tsx b/src/screens/Signup/StepCaptcha.tsx deleted file mode 100644 index 14da8ee93..000000000 --- a/src/screens/Signup/StepCaptcha.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {nanoid} from 'nanoid/non-secure' - -import {createFullHandle} from '#/lib/strings/handles' -import {isWeb} from '#/platform/detection' -import {CaptchaWebView} from '#/view/com/auth/create/CaptchaWebView' -import {ScreenTransition} from '#/screens/Login/ScreenTransition' -import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state' -import {atoms as a, useTheme} from '#/alf' -import {FormError} from '#/components/forms/FormError' - -const CAPTCHA_PATH = '/gate/signup' - -export function StepCaptcha() { - const {_} = useLingui() - const theme = useTheme() - const {state, dispatch} = useSignupContext() - const submit = useSubmitSignup({state, dispatch}) - - const [completed, setCompleted] = React.useState(false) - - const stateParam = React.useMemo(() => nanoid(15), []) - const url = React.useMemo(() => { - const newUrl = new URL(state.serviceUrl) - newUrl.pathname = CAPTCHA_PATH - newUrl.searchParams.set( - 'handle', - createFullHandle(state.handle, state.userDomain), - ) - newUrl.searchParams.set('state', stateParam) - newUrl.searchParams.set('colorScheme', theme.name) - - return newUrl.href - }, [state.serviceUrl, state.handle, state.userDomain, stateParam, theme.name]) - - const onSuccess = React.useCallback( - (code: string) => { - setCompleted(true) - submit(code) - }, - [submit], - ) - - const onError = React.useCallback(() => { - dispatch({ - type: 'setError', - value: _(msg`Error receiving captcha response.`), - }) - }, [_, dispatch]) - - return ( - - - - {!completed ? ( - - ) : ( - - )} - - - - - ) -} - -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/screens/Signup/StepCaptcha/CaptchaWebView.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx new file mode 100644 index 000000000..50918c4ce --- /dev/null +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import {StyleSheet} from 'react-native' +import {WebView, WebViewNavigation} from 'react-native-webview' +import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' + +import {SignupState} from '#/screens/Signup/state' + +const ALLOWED_HOSTS = [ + 'bsky.social', + 'bsky.app', + 'staging.bsky.app', + 'staging.bsky.dev', + 'js.hcaptcha.com', + 'newassets.hcaptcha.com', + 'api2.hcaptcha.com', +] + +export function CaptchaWebView({ + url, + stateParam, + state, + onSuccess, + onError, +}: { + url: string + stateParam: string + state?: SignupState + onSuccess: (code: string) => void + onError: () => void +}) { + const redirectHost = React.useMemo(() => { + if (!state?.serviceUrl) return 'bsky.app' + + return state?.serviceUrl && + new URL(state?.serviceUrl).host === 'staging.bsky.dev' + ? 'staging.bsky.app' + : 'bsky.app' + }, [state?.serviceUrl]) + + const wasSuccessful = React.useRef(false) + + const onShouldStartLoadWithRequest = React.useCallback( + (event: ShouldStartLoadRequest) => { + const urlp = new URL(event.url) + return ALLOWED_HOSTS.includes(urlp.host) + }, + [], + ) + + const onNavigationStateChange = React.useCallback( + (e: WebViewNavigation) => { + if (wasSuccessful.current) return + + const urlp = new URL(e.url) + if (urlp.host !== redirectHost) return + + const code = urlp.searchParams.get('code') + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError() + return + } + + wasSuccessful.current = true + onSuccess(code) + }, + [redirectHost, stateParam, onSuccess, onError], + ) + + return ( + + ) +} + +const styles = StyleSheet.create({ + webview: { + flex: 1, + backgroundColor: 'transparent', + borderRadius: 10, + }, +}) diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx new file mode 100644 index 000000000..7791a58dd --- /dev/null +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import {StyleSheet} from 'react-native' + +// @ts-ignore web only, we will always redirect to the app on web (CORS) +const REDIRECT_HOST = new URL(window.location.href).host + +export function CaptchaWebView({ + url, + stateParam, + onSuccess, + onError, +}: { + url: string + stateParam: string + onSuccess: (code: string) => void + onError: () => void +}) { + const onLoad = React.useCallback(() => { + // @ts-ignore web + const frame: HTMLIFrameElement = document.getElementById( + 'captcha-iframe', + ) as HTMLIFrameElement + + try { + // @ts-ignore web + const href = frame?.contentWindow?.location.href + if (!href) return + const urlp = new URL(href) + + // This shouldn't happen with CORS protections, but for good measure + if (urlp.host !== REDIRECT_HOST) return + + const code = urlp.searchParams.get('code') + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError() + return + } + onSuccess(code) + } catch (e) { + // We don't need to handle this + } + }, [stateParam, onSuccess, onError]) + + return ( +