import {useCallback, useRef, useState} from 'react' import { ActivityIndicator, Keyboard, Pressable, type TextInput, View, } from 'react-native' import { ComAtprotoServerCreateSession, type ComAtprotoServerDescribeServer, } from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {createFullHandle} from '#/lib/strings/handles' import {isValidDomain} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' import {useSessionApi} from '#/state/session' import {getWebOAuthClient} from '#/state/session/oauth-web-client' 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 type LoginMode = 'oauth' | 'legacy' /** * Web-specific LoginForm with two tabs: * - OAuth (default): handle-only flow, redirects to PDS authorization server * - Legacy sign-in: username + password, for operations that may not support OAuth */ export const LoginForm = ({ error, serviceUrl, serviceDescription, initialHandle, setError, setServiceUrl, onPressRetryConnect, onPressBack, onPressForgotPassword, onAttemptSuccess, onAttemptFailed, debouncedResolveService, isResolvingService, }: { error: string serviceUrl?: string | undefined serviceDescription: ServiceDescription | undefined initialHandle: string setError: (v: string) => void setServiceUrl: (v: string) => void onPressRetryConnect: () => void onPressBack: () => void onPressForgotPassword: () => void onAttemptSuccess: () => void onAttemptFailed: () => void debouncedResolveService: (identifier: string) => void isResolvingService: boolean }) => { const t = useTheme() const [mode, setMode] = useState('oauth') const [isProcessing, setIsProcessing] = useState(false) const switchMode = (next: LoginMode) => { if (next === mode) return setError('') setMode(next) } return ( Sign in}> switchMode('oauth')} style={[ a.flex_1, a.align_center, a.py_sm, a.border_b, {borderBottomWidth: 2}, mode === 'oauth' ? {borderBottomColor: t.palette.primary_500} : {borderBottomColor: 'transparent'}, ]}> OAuth switchMode('legacy')} style={[ a.flex_1, a.align_center, a.py_sm, a.border_b, {borderBottomWidth: 2}, mode === 'legacy' ? {borderBottomColor: t.palette.primary_500} : {borderBottomColor: 'transparent'}, ]}> Legacy sign-in {mode === 'oauth' ? ( ) : ( )} ) } function OAuthLoginFields({ error, initialHandle, setError, isProcessing, setIsProcessing, onPressBack, }: { error: string initialHandle: string setError: (v: string) => void isProcessing: boolean setIsProcessing: (v: boolean) => void onPressBack: () => void }) { const {_} = useLingui() const identifierValueRef = useRef(initialHandle || '') const onPressNext = async () => { if (isProcessing) return Keyboard.dismiss() setError('') const identifier = identifierValueRef.current.trim() if (!identifier) { setError(_(msg`Please enter your username or handle`)) return } setIsProcessing(true) try { const client = getWebOAuthClient() await client.signIn(identifier) // Browser will redirect to authorization server } catch (e: any) { const errMsg = e.toString() setIsProcessing(false) if (isNetworkError(e)) { logger.warn('Failed to start OAuth sign-in due to network error', { error: errMsg, }) setError( _( msg`Unable to contact your service. Please check your Internet connection.`, ), ) } else { logger.warn('Failed to start OAuth sign-in', {error: errMsg}) setError(cleanError(errMsg)) } } } return ( <> Account { identifierValueRef.current = v }} onSubmitEditing={onPressNext} blurOnSubmit={false} editable={!isProcessing} accessibilityHint={_( msg`Enter your handle (e.g. alice.bsky.social)`, )} /> ) } function LegacyLoginFields({ error, serviceUrl, serviceDescription, initialHandle, setError, setServiceUrl, onPressRetryConnect, onPressBack, onPressForgotPassword, onAttemptSuccess, onAttemptFailed, debouncedResolveService, isResolvingService, isProcessing, setIsProcessing, }: { error: string serviceUrl?: string | undefined serviceDescription: ServiceDescription | undefined initialHandle: string setError: (v: string) => void setServiceUrl: (v: string) => void onPressRetryConnect: () => void onPressBack: () => void onPressForgotPassword: () => void onAttemptSuccess: () => void onAttemptFailed: () => void debouncedResolveService: (identifier: string) => void isResolvingService: boolean isProcessing: boolean setIsProcessing: (v: boolean) => void }) { const t = useTheme() const {_} = useLingui() const {login} = useSessionApi() const {setShowLoggedOut} = useLoggedOutViewControls() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const [errorField, setErrorField] = useState< 'none' | 'identifier' | 'password' | '2fa' >('none') const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false) const identifierValueRef = useRef(initialHandle || '') const passwordValueRef = useRef('') const [authFactorToken, setAuthFactorToken] = useState('') const identifierRef = useRef(null) const passwordRef = useRef(null) const onPressSelectService = useCallback(() => { Keyboard.dismiss() }, []) const onPressNext = async () => { if (isProcessing || isResolvingService || serviceUrl === undefined) return Keyboard.dismiss() setError('') setErrorField('none') const identifier = identifierValueRef.current.toLowerCase().trim() const password = passwordValueRef.current if (!identifier) { setError(_(msg`Please enter your username`)) setErrorField('identifier') return } if (!password) { setError(_(msg`Please enter your password`)) return } setIsProcessing(true) try { let fullIdent = identifier if ( !identifier.includes('@') && !identifier.includes('.') && 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], ) } } await login( { service: serviceUrl, identifier: fullIdent, password, authFactorToken: authFactorToken.trim(), }, 'LoginForm', ) onAttemptSuccess() setShowLoggedOut(false) setHasCheckedForStarterPack(true) } catch (e: any) { const errMsg = e.toString() 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.`)) setErrorField('2fa') } 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 ( <> Hosting provider {isResolvingService && ( )} Account { identifierValueRef.current = v const id = v.trim() if (!id) return if ( id.startsWith('did:') || (!id.includes('@') && isValidDomain(id)) ) { debouncedResolveService(id) } if (errorField) setErrorField('none') }} onSubmitEditing={() => { passwordRef.current?.focus() }} blurOnSubmit={false} editable={!isProcessing} accessibilityHint={_( msg`Enter the username or email address you used when you created your account`, )} /> { passwordValueRef.current = v if (errorField) setErrorField('none') }} onSubmitEditing={onPressNext} blurOnSubmit={false} editable={!isProcessing} accessibilityHint={_(msg`Enter your password`)} /> {isAuthFactorTokenNeeded && ( 2FA Confirmation { setAuthFactorToken(text) if (errorField) setErrorField('none') }} onSubmitEditing={onPressNext} editable={!isProcessing} accessibilityHint={_( msg`Input the code which has been emailed to you`, )} style={{ textTransform: authFactorToken === '' ? 'none' : 'uppercase', }} /> Check your email for a sign in code and enter it here. )} {!serviceDescription && error ? ( ) : !serviceDescription && serviceUrl !== undefined ? ( ) : ( )} ) }