alf the login form

zio/stable
Samuel Newman 2024-03-13 23:34:01 +00:00
parent f5b39f2755
commit 9f5289a101
7 changed files with 169 additions and 154 deletions

View File

@ -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<TextInput> | null
@ -128,6 +129,7 @@ export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
value: string
onChangeText: (value: string) => void
isInvalid?: boolean
inputRef?: React.RefObject<TextInput>
}
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 (
<>
<Component
accessibilityHint={undefined}
{...rest}
accessibilityLabel={label}
ref={ctx.inputRef}
ref={refs}
value={value}
onChangeText={onChangeText}
onFocus={ctx.onFocus}
onBlur={ctx.onBlur}
placeholder={placeholder || label}
placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
hitSlop={HITSLOP_20}
style={[
a.relative,

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
})

View File

@ -0,0 +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',
})

View File

@ -13,7 +13,7 @@ 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 {atoms as a, useBreakpoints, 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'
@ -106,6 +106,7 @@ export const ChooseAccountForm = ({
const {accounts, currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
const {gtMobile} = useBreakpoints()
React.useEffect(() => {
screen('Choose Account')
@ -133,7 +134,9 @@ export const ChooseAccountForm = ({
return (
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
<Text style={[a.mt_md, a.mb_lg, a.font_bold]}>
<View style={!gtMobile && a.px_lg}>
<Text
style={[a.mt_md, a.mb_lg, a.font_bold, t.atoms.text_contrast_medium]}>
<Trans>Sign in as...</Trans>
</Text>
<Group>
@ -152,7 +155,8 @@ export const ChooseAccountForm = ({
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}]}>
<View
style={[a.flex_row, a.flex_row, a.align_center, {height: 48}]}>
<Text
style={[
a.align_baseline,
@ -174,10 +178,11 @@ export const ChooseAccountForm = ({
color="secondary"
size="small"
onPress={onPressBack}>
<Trans>Back</Trans>
{_(msg`Back`)}
</Button>
<View style={[a.flex_1]} />
</View>
</View>
</ScrollView>
)
}

View File

@ -6,28 +6,31 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
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 {styles} from '../../view/com/auth/login/styles'
import {useLingui} from '@lingui/react'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
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
@ -40,7 +43,6 @@ export const LoginForm = ({
setServiceUrl,
onPressRetryConnect,
onPressBack,
onPressForgotPassword,
}: {
error: string
serviceUrl: string
@ -53,8 +55,7 @@ export const LoginForm = ({
onPressForgotPassword: () => void
}) => {
const {track} = useAnalytics()
const pal = usePalette('default')
const theme = useTheme()
const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle)
const [password, setPassword] = useState<string>('')
@ -62,6 +63,7 @@ export const LoginForm = ({
const {_} = useLingui()
const {login} = useSessionApi()
const serverInputControl = useDialogControl()
const {gtMobile} = useBreakpoints()
const onPressSelectService = () => {
serverInputControl.open()
@ -127,55 +129,54 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password
return (
<View testID="loginForm">
<View testID="loginForm" style={[a.gap_lg, !gtMobile && a.px_lg]}>
<ServerInputDialog
control={serverInputControl}
onSelect={setServiceUrl}
/>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Sign into</Trans>
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
/>
<View>
<TextField.Label>
<Trans>Hosting provider</Trans>
</TextField.Label>
<TouchableOpacity
testID="loginSelectServiceButton"
style={styles.textBtn}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)}
accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
{toNiceDomain(serviceUrl)}
</Text>
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
style={[
a.w_full,
a.flex_row,
a.align_center,
a.rounded_sm,
a.px_md,
a.gap_xs,
{paddingVertical: isAndroid ? 14 : 9},
t.atoms.bg_contrast_25,
]}
onPress={onPressSelectService}>
<TextField.Icon icon={Globe} />
<Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
<View
style={[
a.rounded_sm,
t.atoms.bg_contrast_100,
{marginLeft: 'auto', left: 6, padding: 6},
]}>
<Pencil
style={{color: t.palette.contrast_500}}
height={18}
width={18}
/>
</View>
</TouchableOpacity>
</View>
</View>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<View>
<TextField.Label>
<Trans>Account</Trans>
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="at"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
</TextField.Label>
<TextField.Root>
<TextField.Icon icon={At} />
<TextField.Input
testID="loginUsernameInput"
style={[pal.text, styles.textInput]}
placeholder={_(msg`Username or email address`)}
placeholderTextColor={pal.colors.textLight}
label={_(msg`Username or email address`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
@ -186,35 +187,29 @@ export const LoginForm = ({
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`,
)}
/>
</TextField.Root>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="lock"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
<View>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
testID="loginPasswordInput"
ref={passwordInputRef}
style={[pal.text, styles.textInput]}
placeholder="Password"
placeholderTextColor={pal.colors.textLight}
inputRef={passwordInputRef}
label={_(msg`Password`)}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
keyboardAppearance={theme.colorScheme}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
@ -223,14 +218,13 @@ export const LoginForm = ({
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityLabel={_(msg`Password`)}
accessibilityHint={
identifier === ''
? _(msg`Input your password`)
: _(msg`Input the password tied to ${identifier}`)
}
/>
<TouchableOpacity
{/* <TouchableOpacity
testID="forgotPasswordButton"
style={styles.textInputInnerBtn}
onPress={onPressForgotPassword}
@ -240,57 +234,57 @@ export const LoginForm = ({
<Text style={pal.link}>
<Trans>Forgot</Trans>
</Text>
</TouchableOpacity>
</View>
</TouchableOpacity> */}
</TextField.Root>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<View style={[styles.error, {marginHorizontal: 0}]}>
<Warning style={s.white} size="sm" />
<View style={(a.flex_1, a.ml_sm)}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={[a.flex_row, a.align_center]}>
<Button
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
{_(msg`Back`)}
</Button>
<View style={s.flex1} />
{!serviceDescription && error ? (
<TouchableOpacity
<Button
testID="loginRetryButton"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint={_(msg`Retries login`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans>
</Text>
</TouchableOpacity>
label={_(msg`Retry`)}
accessibilityHint={_(msg`Retries login`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressRetryConnect}>
{_(msg`Retry`)}
</Button>
) : !serviceDescription ? (
<>
<ActivityIndicator />
<Text type="xl" style={[pal.textLight, s.pl10]}>
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : isProcessing ? (
<ActivityIndicator />
) : isReady ? (
<TouchableOpacity
testID="loginNextButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
</TouchableOpacity>
<Button
label={_(msg`Next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid"
color="primary"
size="small"
onPress={onPressNext}>
{_(msg`Next`)}
</Button>
) : undefined}
</View>
</View>

View File

@ -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,

View File

@ -67,7 +67,7 @@ export function ServerInputDialog({
return (
<Dialog.Outer
control={control}
nativeOptions={{sheet: {snapPoints: ['100%']}}}
nativeOptions={{sheet: {snapPoints: ['80', '100%']}}}
onClose={onClose}>
<Dialog.Handle />