bsky-app/src/view/com/auth/login/LoginForm.tsx
Ansh 8e541d753a
Check Analytics (#2106)
* fix sign in event tracking

* add missing analytics events

* add more missing analytics

* fix like and unrepost event tracking

* reset onEndReachedThreshold
2023-12-06 09:45:01 -08:00

288 lines
9.6 KiB
TypeScript

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 {useModalControls} from '#/state/modals'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
export const LoginForm = ({
error,
serviceUrl,
serviceDescription,
initialHandle,
setError,
setServiceUrl,
onPressRetryConnect,
onPressBack,
onPressForgotPassword,
}: {
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
initialHandle: string
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressRetryConnect: () => void
onPressBack: () => void
onPressForgotPassword: () => void
}) => {
const {track} = useAnalytics()
const pal = usePalette('default')
const theme = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle)
const [password, setPassword] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui()
const {openModal} = useModalControls()
const {login} = useSessionApi()
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
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()
logger.warn('Failed to login', {error: e})
setIsProcessing(false)
if (errMsg.includes('Authentication Required')) {
setError(_(msg`Invalid username or password`))
} else if (isNetworkError(e)) {
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else {
setError(cleanError(errMsg))
}
}
}
const isReady = !!serviceDescription && !!identifier && !!password
return (
<View testID="loginForm">
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Sign into</Trans>
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TouchableOpacity
testID="loginSelectServiceButton"
style={styles.textBtn}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)}
accessibilityHint="Sets server for the Bluesky client">
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
{toNiceDomain(serviceUrl)}
</Text>
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
</View>
</View>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Account</Trans>
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="at"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="loginUsernameInput"
style={[pal.text, styles.textInput]}
placeholder={_(msg`Username or email address`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
onSubmitEditing={() => {
passwordInputRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
keyboardAppearance={theme.colorScheme}
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing}
accessibilityLabel={_(msg`Username or email address`)}
accessibilityHint="Input the username or email address you used at signup"
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="lock"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="loginPasswordInput"
ref={passwordInputRef}
style={[pal.text, styles.textInput]}
placeholder="Password"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
keyboardAppearance={theme.colorScheme}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityLabel={_(msg`Password`)}
accessibilityHint={
identifier === ''
? 'Input your password'
: `Input the password tied to ${identifier}`
}
/>
<TouchableOpacity
testID="forgotPasswordButton"
style={styles.textInputInnerBtn}
onPress={onPressForgotPassword}
accessibilityRole="button"
accessibilityLabel={_(msg`Forgot password`)}
accessibilityHint="Opens password reset form">
<Text style={pal.link}>
<Trans>Forgot</Trans>
</Text>
</TouchableOpacity>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{!serviceDescription && error ? (
<TouchableOpacity
testID="loginRetryButton"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries login">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans>
</Text>
</TouchableOpacity>
) : !serviceDescription ? (
<>
<ActivityIndicator />
<Text type="xl" style={[pal.textLight, s.pl10]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : isProcessing ? (
<ActivityIndicator />
) : isReady ? (
<TouchableOpacity
testID="loginNextButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
</TouchableOpacity>
) : undefined}
</View>
</View>
)
}