diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/alf/atoms.ts | 6 | ||||
-rw-r--r-- | src/screens/Login/ChooseAccountForm.tsx | 183 | ||||
-rw-r--r-- | src/screens/Login/index.tsx | 166 | ||||
-rw-r--r-- | src/view/com/auth/LoggedOut.tsx | 16 | ||||
-rw-r--r-- | src/view/com/auth/login/ChooseAccountForm.tsx | 158 | ||||
-rw-r--r-- | src/view/com/auth/login/Login.tsx | 164 |
6 files changed, 363 insertions, 330 deletions
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 ( + <View + style={[ + a.rounded_md, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + ]}> + {flattenReactChildren(children).map((child, i) => { + return React.isValidElement(child) ? ( + <React.Fragment key={i}> + {i > 0 ? ( + <View style={[a.border_b, t.atoms.border_contrast_low]} /> + ) : null} + {React.cloneElement(child, { + // @ts-ignore + style: { + borderRadius: 0, + borderWidth: 0, + }, + })} + </React.Fragment> + ) : null + })} + </View> + ) +} + +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 ( + <TouchableOpacity + testID={`chooseAccountBtn-${account.handle}`} + key={account.did} + style={[a.flex_1]} + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={_(msg`Sign in as ${account.handle}`)} + accessibilityHint={_(msg`Double tap to sign in`)}> + <View style={[a.flex_1, a.flex_row, a.align_center, {height: 48}]}> + <View style={a.p_md}> + <UserAvatar avatar={profile?.avatar} size={24} /> + </View> + <Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}> + <Text style={[a.font_bold]}> + {profile?.displayName || account.handle}{' '} + </Text> + <Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text> + </Text> + {isCurrentAccount ? ( + <Check size="sm" style={[{color: colors.green3}, a.mr_md]} /> + ) : ( + <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> + )} + </View> + </TouchableOpacity> + ) +} +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 ( + <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> + <Text style={[a.mt_md, a.mb_lg, a.font_bold]}> + <Trans>Sign in as...</Trans> + </Text> + <Group> + {accounts.map(account => ( + <AccountItem + key={account.did} + account={account} + onSelect={onSelect} + isCurrentAccount={account.did === currentAccount?.did} + /> + ))} + <TouchableOpacity + testID="chooseNewAccountBtn" + style={[a.flex_1]} + onPress={() => onSelectAccount(undefined)} + accessibilityRole="button" + accessibilityLabel={_(msg`Login to account that is not listed`)} + accessibilityHint=""> + <View style={[a.flex_row, a.flex_row, a.align_center, {height: 48}]}> + <Text + style={[ + a.align_baseline, + a.flex_1, + a.flex_row, + a.py_sm, + {paddingLeft: 48}, + ]}> + <Trans>Other account</Trans> + </Text> + <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> + </View> + </TouchableOpacity> + </Group> + <View style={[a.flex_row, a.mt_lg]}> + <Button + label={_(msg`Back`)} + variant="solid" + color="secondary" + size="small" + onPress={onPressBack}> + <Trans>Back</Trans> + </Button> + <View style={[a.flex_1]} /> + </View> + </ScrollView> + ) +} 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<string>('') + const [serviceUrl, setServiceUrl] = React.useState<string>( + requestedAccount?.service || DEFAULT_SERVICE, + ) + const [initialHandle, setInitialHandle] = React.useState<string>( + requestedAccount?.handle || '', + ) + const [currentForm, setCurrentForm] = React.useState<Forms>( + requestedAccount + ? Forms.Login + : accounts.length + ? Forms.ChooseAccount + : Forms.Login, + ) + + const { + data: serviceDescription, + error: serviceError, + refetch: refetchService, + } = useServiceQuery(serviceUrl) + + const onSelectAccount = (account?: SessionAccount) => { + if (account?.service) { + setServiceUrl(account.service) + } + setInitialHandle(account?.handle || '') + setCurrentForm(Forms.Login) + } + + const gotoForm = (form: Forms) => () => { + setError('') + setCurrentForm(form) + } + + 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 = ( + <LoginForm + error={error} + serviceUrl={serviceUrl} + serviceDescription={serviceDescription} + initialHandle={initialHandle} + setError={setError} + setServiceUrl={setServiceUrl} + onPressBack={onPressBack} + onPressForgotPassword={onPressForgotPassword} + onPressRetryConnect={onPressRetryConnect} + /> + ) + break + case Forms.ChooseAccount: + title = _(msg`Sign in`) + description = _(msg`Select from an existing account`) + content = ( + <ChooseAccountForm + onSelectAccount={onSelectAccount} + onPressBack={onPressBack} + /> + ) + break + case Forms.ForgotPassword: + title = _(msg`Forgot Password`) + description = _(msg`Let's get your password reset!`) + content = ( + <ForgotPasswordForm + error={error} + serviceUrl={serviceUrl} + serviceDescription={serviceDescription} + setError={setError} + setServiceUrl={setServiceUrl} + onPressBack={gotoForm(Forms.Login)} + onEmailSent={gotoForm(Forms.SetNewPassword)} + /> + ) + break + case Forms.SetNewPassword: + title = _(msg`Forgot Password`) + description = _(msg`Let's get your password reset!`) + content = ( + <SetNewPasswordForm + error={error} + serviceUrl={serviceUrl} + setError={setError} + 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 = <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} /> + break + } + + return ( + <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}> + <LoggedOutLayout leadin="" title={title} description={description}> + {content} + </LoggedOutLayout> + </KeyboardAvoidingView> + ) +} 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 ( - <TouchableOpacity - testID={`chooseAccountBtn-${account.handle}`} - key={account.did} - style={[pal.view, pal.border, styles.account]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={_(msg`Sign in as ${account.handle}`)} - accessibilityHint={_(msg`Double tap to sign in`)}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <View style={s.p10}> - <UserAvatar avatar={profile?.avatar} size={30} /> - </View> - <Text style={styles.accountText}> - <Text type="lg-bold" style={pal.text}> - {profile?.displayName || account.handle}{' '} - </Text> - <Text type="lg" style={[pal.textLight]}> - {account.handle} - </Text> - </Text> - {isCurrentAccount ? ( - <FontAwesomeIcon - icon="check" - size={16} - style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]} - /> - ) : ( - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - )} - </View> - </TouchableOpacity> - ) -} -export const ChooseAccountForm = ({ - onSelectAccount, - onPressBack, -}: { - onSelectAccount: (account?: SessionAccount) => void - onPressBack: () => void -}) => { - const {track, screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - const {accounts, currentAccount} = useSession() - const {initSession} = useSessionApi() - const {setShowLoggedOut} = useLoggedOutViewControls() - - React.useEffect(() => { - screen('Choose Account') - }, [screen]) - - const onSelect = React.useCallback( - async (account: SessionAccount) => { - if (account.accessJwt) { - if (account.did === currentAccount?.did) { - setShowLoggedOut(false) - Toast.show(_(msg`Already signed in as @${account.handle}`)) - } else { - await initSession(account) - track('Sign In', {resumedSession: true}) - setTimeout(() => { - Toast.show(_(msg`Signed in as @${account.handle}`)) - }, 100) - } - } else { - onSelectAccount(account) - } - }, - [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _], - ) - - return ( - <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> - <Text - type="2xl-medium" - style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}> - <Trans>Sign in as...</Trans> - </Text> - {accounts.map(account => ( - <AccountItem - key={account.did} - account={account} - onSelect={onSelect} - isCurrentAccount={account.did === currentAccount?.did} - /> - ))} - <TouchableOpacity - testID="chooseNewAccountBtn" - style={[pal.view, pal.border, styles.account, styles.accountLast]} - onPress={() => onSelectAccount(undefined)} - accessibilityRole="button" - accessibilityLabel={_(msg`Login to account that is not listed`)} - accessibilityHint=""> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <Text style={[styles.accountText, styles.accountTextOther]}> - <Text type="lg" style={pal.text}> - <Trans>Other account</Trans> - </Text> - </Text> - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - </View> - </TouchableOpacity> - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - </View> - </ScrollView> - ) -} diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx deleted file mode 100644 index bc931ac04..000000000 --- a/src/view/com/auth/login/Login.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, {useState, useEffect} from 'react' -import {KeyboardAvoidingView} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' -import {DEFAULT_SERVICE} from '#/lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {logger} from '#/logger' -import {ChooseAccountForm} from './ChooseAccountForm' -import {LoginForm} from './LoginForm' -import {ForgotPasswordForm} from './ForgotPasswordForm' -import {SetNewPasswordForm} from './SetNewPasswordForm' -import {PasswordUpdatedForm} from './PasswordUpdatedForm' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' -import {useSession, SessionAccount} from '#/state/session' -import {useServiceQuery} from '#/state/queries/service' -import {useLoggedOutView} from '#/state/shell/logged-out' - -enum Forms { - Login, - ChooseAccount, - ForgotPassword, - SetNewPassword, - PasswordUpdated, -} - -export const Login = ({onPressBack}: {onPressBack: () => void}) => { - const {_} = useLingui() - const pal = usePalette('default') - - const {accounts} = useSession() - const {track} = useAnalytics() - const {requestedAccountSwitchTo} = useLoggedOutView() - const requestedAccount = accounts.find( - a => a.did === requestedAccountSwitchTo, - ) - - const [error, setError] = useState<string>('') - const [serviceUrl, setServiceUrl] = useState<string>( - requestedAccount?.service || DEFAULT_SERVICE, - ) - const [initialHandle, setInitialHandle] = useState<string>( - requestedAccount?.handle || '', - ) - const [currentForm, setCurrentForm] = useState<Forms>( - requestedAccount - ? Forms.Login - : accounts.length - ? Forms.ChooseAccount - : Forms.Login, - ) - - const { - data: serviceDescription, - error: serviceError, - refetch: refetchService, - } = useServiceQuery(serviceUrl) - - const onSelectAccount = (account?: SessionAccount) => { - if (account?.service) { - setServiceUrl(account.service) - } - setInitialHandle(account?.handle || '') - setCurrentForm(Forms.Login) - } - - const gotoForm = (form: Forms) => () => { - setError('') - setCurrentForm(form) - } - - useEffect(() => { - if (serviceError) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - logger.warn(`Failed to fetch service description for ${serviceUrl}`, { - error: String(serviceError), - }) - } else { - setError('') - } - }, [serviceError, serviceUrl, _]) - - const onPressRetryConnect = () => refetchService() - const onPressForgotPassword = () => { - track('Signin:PressedForgotPassword') - setCurrentForm(Forms.ForgotPassword) - } - - return ( - <KeyboardAvoidingView testID="signIn" behavior="padding" style={pal.view}> - {currentForm === Forms.Login ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Sign in`)} - description={_(msg`Enter your username and password`)}> - <LoginForm - error={error} - serviceUrl={serviceUrl} - serviceDescription={serviceDescription} - initialHandle={initialHandle} - setError={setError} - setServiceUrl={setServiceUrl} - onPressBack={onPressBack} - onPressForgotPassword={onPressForgotPassword} - onPressRetryConnect={onPressRetryConnect} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.ChooseAccount ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Sign in as...`)} - description={_(msg`Select from an existing account`)}> - <ChooseAccountForm - onSelectAccount={onSelectAccount} - onPressBack={onPressBack} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.ForgotPassword ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Forgot Password`)} - description={_(msg`Let's get your password reset!`)}> - <ForgotPasswordForm - error={error} - serviceUrl={serviceUrl} - serviceDescription={serviceDescription} - setError={setError} - setServiceUrl={setServiceUrl} - onPressBack={gotoForm(Forms.Login)} - onEmailSent={gotoForm(Forms.SetNewPassword)} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.SetNewPassword ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Forgot Password`)} - description={_(msg`Let's get your password reset!`)}> - <SetNewPasswordForm - error={error} - serviceUrl={serviceUrl} - setError={setError} - onPressBack={gotoForm(Forms.ForgotPassword)} - onPasswordSet={gotoForm(Forms.PasswordUpdated)} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.PasswordUpdated ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Password updated`)} - description={_(msg`You can now sign in with your new password.`)}> - <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} /> - </LoggedOutLayout> - ) : undefined} - </KeyboardAvoidingView> - ) -} |