import React, {useState, useEffect} from 'react' import { ActivityIndicator, Keyboard, KeyboardAvoidingView, StyleSheet, TextInput, TouchableOpacity, View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as EmailValidator from 'email-validator' import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' import {Logo} from './Logo' import {Text} from '../util/text/Text' import {s, colors} from '../../lib/styles' import {createFullHandle, toNiceDomain} from '../../../lib/strings' import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state' import {ServiceDescription} from '../../../state/models/session' import {ServerInputModal} from '../../../state/models/shell-ui' import {isNetworkError} from '../../../lib/errors' enum Forms { Login, ForgotPassword, SetNewPassword, PasswordUpdated, } export const Signin = ({onPressBack}: {onPressBack: () => void}) => { const store = useStores() const [error, setError] = useState('') const [serviceUrl, setServiceUrl] = useState(DEFAULT_SERVICE) const [serviceDescription, setServiceDescription] = useState< ServiceDescription | undefined >(undefined) const [currentForm, setCurrentForm] = useState(Forms.Login) const gotoForm = (form: Forms) => () => { setError('') setCurrentForm(form) } useEffect(() => { let aborted = false setError('') store.session.describeService(serviceUrl).then( desc => { if (aborted) return setServiceDescription(desc) }, err => { if (aborted) return store.log.warn( `Failed to fetch service description for ${serviceUrl}`, err.toString(), ) setError( 'Unable to contact your service. Please check your Internet connection.', ) }, ) return () => { aborted = true } }, [store.session, store.log, serviceUrl]) return ( {currentForm === Forms.Login ? ( ) : undefined} {currentForm === Forms.ForgotPassword ? ( ) : undefined} {currentForm === Forms.SetNewPassword ? ( ) : undefined} {currentForm === Forms.PasswordUpdated ? ( ) : undefined} ) } const LoginForm = ({ store, error, serviceUrl, serviceDescription, setError, setServiceUrl, onPressBack, onPressForgotPassword, }: { store: RootStoreModel error: string serviceUrl: string serviceDescription: ServiceDescription | undefined setError: (v: string) => void setServiceUrl: (v: string) => void onPressBack: () => void onPressForgotPassword: () => void }) => { const [isProcessing, setIsProcessing] = useState(false) const [handle, setHandle] = useState('') const [password, setPassword] = useState('') const onPressSelectService = () => { store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) Keyboard.dismiss() } const onPressNext = async () => { setError('') setIsProcessing(true) try { // try to guess the handle if the user just gave their own username let fullHandle = handle if ( serviceDescription && serviceDescription.availableUserDomains.length > 0 ) { let matched = false for (const domain of serviceDescription.availableUserDomains) { if (fullHandle.endsWith(domain)) { matched = true } } if (!matched) { fullHandle = createFullHandle( handle, serviceDescription.availableUserDomains[0], ) } } await store.session.login({ service: serviceUrl, handle: fullHandle, password, }) } catch (e: any) { const errMsg = e.toString() store.log.warn('Failed to login', e.toString()) setIsProcessing(false) if (errMsg.includes('Authentication Required')) { setError('Invalid username or password') } else if (isNetworkError(e)) { setError( 'Unable to contact your service. Please check your Internet connection.', ) } else { setError(errMsg.replace(/^Error:/, '')) } } } return ( <> Sign in to {toNiceDomain(serviceUrl)} Change setHandle((str || '').toLowerCase())} editable={!isProcessing} /> Forgot {error ? ( {error} ) : undefined} Back {!serviceDescription || isProcessing ? ( ) : ( Next )} {!serviceDescription || isProcessing ? ( Connecting... ) : undefined} ) } const ForgotPasswordForm = ({ store, error, serviceUrl, serviceDescription, setError, setServiceUrl, onPressBack, onEmailSent, }: { store: RootStoreModel error: string serviceUrl: string serviceDescription: ServiceDescription | undefined setError: (v: string) => void setServiceUrl: (v: string) => void onPressBack: () => void onEmailSent: () => void }) => { const [isProcessing, setIsProcessing] = useState(false) const [email, setEmail] = useState('') const onPressSelectService = () => { store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) } const onPressNext = async () => { if (!EmailValidator.validate(email)) { return setError('Your email appears to be invalid.') } setError('') setIsProcessing(true) try { const api = AtpApi.service(serviceUrl) as SessionServiceClient await api.com.atproto.account.requestPasswordReset({email}) onEmailSent() } catch (e: any) { const errMsg = e.toString() store.log.warn('Failed to request password reset', e.toString()) setIsProcessing(false) if (isNetworkError(e)) { setError( 'Unable to contact your service. Please check your Internet connection.', ) } else { setError(errMsg.replace(/^Error:/, '')) } } } return ( <> Reset password Enter the email you used to create your account. We'll send you a "reset code" so you can set a new password. {toNiceDomain(serviceUrl)} Change {error ? ( {error} ) : undefined} Back {!serviceDescription || isProcessing ? ( ) : !email ? ( Next ) : ( Next )} {!serviceDescription || isProcessing ? ( Processing... ) : undefined} ) } const SetNewPasswordForm = ({ store, error, serviceUrl, setError, onPressBack, onPasswordSet, }: { store: RootStoreModel error: string serviceUrl: string setError: (v: string) => void onPressBack: () => void onPasswordSet: () => void }) => { const [isProcessing, setIsProcessing] = useState(false) const [resetCode, setResetCode] = useState('') const [password, setPassword] = useState('') const onPressNext = async () => { setError('') setIsProcessing(true) try { const api = AtpApi.service(serviceUrl) as SessionServiceClient await api.com.atproto.account.resetPassword({token: resetCode, password}) onPasswordSet() } catch (e: any) { const errMsg = e.toString() store.log.warn('Failed to set new password', e.toString()) setIsProcessing(false) if (isNetworkError(e)) { setError( 'Unable to contact your service. Please check your Internet connection.', ) } else { setError(errMsg.replace(/^Error:/, '')) } } } return ( <> Set new password You will receive an email with a "reset code." Enter that code here, then enter your new password. {error ? ( {error} ) : undefined} Back {isProcessing ? ( ) : !resetCode || !password ? ( Next ) : ( Next )} {isProcessing ? ( Updating... ) : undefined} ) } const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => { return ( <> Password updated! You can now sign in with your new password. Okay ) } const styles = StyleSheet.create({ screenTitle: { color: colors.white, fontSize: 26, marginBottom: 10, marginHorizontal: 20, }, instructions: { color: colors.white, fontSize: 16, marginBottom: 20, marginHorizontal: 20, }, logoHero: { paddingTop: 30, paddingBottom: 40, }, group: { borderWidth: 1, borderColor: colors.white, borderRadius: 10, marginBottom: 20, marginHorizontal: 20, backgroundColor: colors.blue3, }, groupTitle: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, paddingHorizontal: 12, }, groupContent: { borderTopWidth: 1, borderTopColor: colors.blue1, flexDirection: 'row', alignItems: 'center', }, groupContentIcon: { color: 'white', marginLeft: 10, }, textInput: { flex: 1, width: '100%', backgroundColor: colors.blue3, color: colors.white, paddingVertical: 10, paddingHorizontal: 12, fontSize: 18, borderRadius: 10, }, textInputInnerBtn: { flexDirection: 'row', alignItems: 'center', paddingVertical: 6, paddingHorizontal: 8, marginHorizontal: 6, }, textInputInnerBtnLabel: { color: colors.white, }, textBtnFakeInnerBtn: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.blue2, borderRadius: 6, paddingVertical: 6, paddingHorizontal: 8, marginHorizontal: 6, }, textBtnFakeInnerBtnIcon: { color: colors.white, marginRight: 4, }, textBtnFakeInnerBtnLabel: { color: colors.white, }, error: { borderWidth: 1, borderColor: colors.red5, backgroundColor: colors.red4, flexDirection: 'row', alignItems: 'center', marginTop: -5, marginHorizontal: 20, marginBottom: 15, borderRadius: 8, paddingHorizontal: 8, paddingVertical: 8, }, errorIcon: { borderWidth: 1, borderColor: colors.white, color: colors.white, borderRadius: 30, width: 16, height: 16, alignItems: 'center', justifyContent: 'center', marginRight: 5, }, })