import React, {useRef, useState} from 'react' import { ActivityIndicator, Keyboard, LayoutAnimation, type TextInput, View, } from 'react-native' import { ComAtprotoServerCreateSession, type ComAtprotoServerDescribeServer, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import {isNetworkError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors' import {createFullHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' import {useSessionApi} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {FormError} from '#/components/forms/FormError' import {HostingProvider} from '#/components/forms/HostingProvider' 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 {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {FormContainer} from './FormContainer' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema export const LoginForm = ({ error, serviceUrl, serviceDescription, initialHandle, setError, setServiceUrl, onPressRetryConnect, onPressBack, onPressForgotPassword, onAttemptSuccess, onAttemptFailed, }: { error: string serviceUrl: string serviceDescription: ServiceDescription | undefined initialHandle: string setError: (v: string) => void setServiceUrl: (v: string) => void onPressRetryConnect: () => void onPressBack: () => void onPressForgotPassword: () => void onAttemptSuccess: () => void onAttemptFailed: () => void }) => { const t = useTheme() const [isProcessing, setIsProcessing] = useState(false) const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false) const [isAuthFactorTokenValueEmpty, setIsAuthFactorTokenValueEmpty] = useState(true) const identifierValueRef = useRef(initialHandle || '') const passwordValueRef = useRef('') const authFactorTokenValueRef = useRef('') const passwordRef = useRef(null) const {_} = useLingui() const {login} = useSessionApi() const requestNotificationsPermission = useRequestNotificationsPermission() const {setShowLoggedOut} = useLoggedOutViewControls() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() }, []) const onPressNext = async () => { if (isProcessing) return Keyboard.dismiss() LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setError('') const identifier = identifierValueRef.current.toLowerCase().trim() const password = passwordValueRef.current const authFactorToken = authFactorTokenValueRef.current if (!identifier) { setError(_(msg`Please enter your username`)) return } if (!password) { setError(_(msg`Please enter your password`)) return } 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, authFactorToken: authFactorToken.trim(), }, 'LoginForm', ) onAttemptSuccess() setShowLoggedOut(false) setHasCheckedForStarterPack(true) requestNotificationsPermission('Login') } catch (e: any) { const errMsg = e.toString() LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setIsProcessing(false) if ( e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError ) { setIsAuthFactorTokenNeeded(true) } else { onAttemptFailed() if (errMsg.includes('Token is invalid')) { logger.debug('Failed to login due to invalid 2fa token', { error: errMsg, }) setError(_(msg`Invalid 2FA confirmation code.`)) } else if ( errMsg.includes('Authentication Required') || errMsg.includes('Invalid identifier or password') ) { logger.debug('Failed to login due to invalid credentials', { error: errMsg, }) setError(_(msg`Incorrect 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)) } } } } return ( Sign in}> Hosting provider Account { identifierValueRef.current = v }} onSubmitEditing={() => { passwordRef.current?.focus() }} blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field editable={!isProcessing} accessibilityHint={_( msg`Enter the username or email address you used when you created your account`, )} /> { passwordValueRef.current = v }} 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} accessibilityHint={_(msg`Enter your password`)} /> {isAuthFactorTokenNeeded && ( 2FA Confirmation { setIsAuthFactorTokenValueEmpty(v === '') authFactorTokenValueRef.current = v }} onSubmitEditing={onPressNext} editable={!isProcessing} accessibilityHint={_( msg`Input the code which has been emailed to you`, )} style={[ { textTransform: isAuthFactorTokenValueEmpty ? 'none' : 'uppercase', }, ]} /> Check your email for a sign in code and enter it here. )} {!serviceDescription && error ? ( ) : !serviceDescription ? ( <> Connecting... ) : ( )} ) }