import React, {useState, useEffect} from 'react'
import {
ActivityIndicator,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native'
import Svg, {Circle, Line, Text as SvgText} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import * as EmailValidator from 'email-validator'
import {observer} from 'mobx-react-lite'
import {Picker} from '../com/util/Picker'
import {s, colors} from '../lib/styles'
import {makeValidHandle, createFullHandle, toNiceDomain} from '../lib/strings'
import {useStores, DEFAULT_SERVICE} from '../../state'
import {ServiceDescription} from '../../state/models/session'
import {ServerInputModel} from '../../state/models/shell-ui'
import {ComAtprotoAccountCreate} from '../../third-party/api/index'
enum ScreenState {
SigninOrCreateAccount,
Signin,
CreateAccount,
}
const Logo = () => {
return (
)
}
const SigninOrCreateAccount = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const winDim = useWindowDimensions()
const halfWidth = winDim.width / 2
return (
<>
Bluesky
[ private beta ]
Create a new account
or
Sign in
>
)
}
const Signin = ({onPressBack}: {onPressBack: () => void}) => {
const store = useStores()
const [isProcessing, setIsProcessing] = useState(false)
const [serviceUrl, setServiceUrl] = useState(DEFAULT_SERVICE)
const [serviceDescription, setServiceDescription] = useState<
ServiceDescription | undefined
>(undefined)
const [error, setError] = useState('')
const [handle, setHandle] = useState('')
const [password, setPassword] = useState('')
useEffect(() => {
let aborted = false
setError('')
console.log('Fetching service description', serviceUrl)
store.session.describeService(serviceUrl).then(
desc => {
if (aborted) return
setServiceDescription(desc)
},
err => {
if (aborted) return
console.error(err)
setError(
'Unable to contact your service. Please check your Internet connection.',
)
},
)
return () => {
aborted = true
}
}, [serviceUrl])
const onPressSelectService = () => {
store.shell.openModal(new ServerInputModel(serviceUrl, setServiceUrl))
}
const onPressNext = async () => {
setError('')
setIsProcessing(true)
// try to guess the handle if the user just gave their own username
try {
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()
console.log(e)
setIsProcessing(false)
if (errMsg.includes('Authentication Required')) {
setError('Invalid username or password')
} else if (errMsg.includes('Network request failed')) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(errMsg.replace(/^Error:/, ''))
}
}
}
return (
Sign in to {toNiceDomain(serviceUrl)}
{error ? (
{error}
) : undefined}
setHandle((str || '').toLowerCase())}
editable={!isProcessing}
/>
Back
{isProcessing ? (
) : (
Next
)}
)
}
const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
const store = useStores()
const [isProcessing, setIsProcessing] = useState(false)
const [serviceUrl, setServiceUrl] = useState(DEFAULT_SERVICE)
const [error, setError] = useState('')
const [serviceDescription, setServiceDescription] = useState<
ServiceDescription | undefined
>(undefined)
const [userDomain, setUserDomain] = useState('')
const [inviteCode, setInviteCode] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [handle, setHandle] = useState('')
useEffect(() => {
let aborted = false
setError('')
console.log('Fetching service description', serviceUrl)
store.session.describeService(serviceUrl).then(
desc => {
if (aborted) return
setServiceDescription(desc)
setUserDomain(desc.availableUserDomains[0])
},
err => {
if (aborted) return
console.error(err)
setError(
'Unable to contact your service. Please check your Internet connection.',
)
},
)
return () => {
aborted = true
}
}, [serviceUrl])
const onPressSelectService = () => {
store.shell.openModal(new ServerInputModel(serviceUrl, setServiceUrl))
}
const onPressNext = async () => {
if (!email) {
return setError('Please enter your email.')
}
if (!EmailValidator.validate(email)) {
return setError('Your email appears to be invalid.')
}
if (!password) {
return setError('Please choose your password.')
}
if (!handle) {
return setError('Please choose your username.')
}
setError('')
setIsProcessing(true)
try {
await store.session.createAccount({
service: serviceUrl,
email,
handle: createFullHandle(handle, userDomain),
password,
inviteCode,
})
} catch (e: any) {
let errMsg = e.toString()
if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) {
errMsg =
'Invite code not accepted. Check that you input it correctly and try again.'
}
console.log(e)
setIsProcessing(false)
setError(errMsg.replace(/^Error:/, ''))
}
}
const InitialLoadView = () => (
<>
{error ? (
<>
{error}
Back
>
) : (
)}
>
)
return (
{serviceDescription ? (
<>
{error ? (
{error}
) : undefined}
Create a new account
{toNiceDomain(serviceUrl)}
{serviceDescription?.inviteCodeRequired ? (
) : undefined}
Choose your username
setHandle(makeValidHandle(v))}
editable={!isProcessing}
/>
{serviceDescription.availableUserDomains.length > 1 && (
({
label: `.${d}`,
value: d,
}))}
onChange={itemValue => setUserDomain(itemValue)}
enabled={!isProcessing}
/>
)}
Your full username will be{' '}
@{createFullHandle(handle, userDomain)}
Back
{isProcessing ? (
) : (
Next
)}
>
) : (
)}
)
}
export const Login = observer(
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
const [screenState, setScreenState] = useState(
ScreenState.SigninOrCreateAccount,
)
return (
{screenState === ScreenState.SigninOrCreateAccount ? (
setScreenState(ScreenState.Signin)}
onPressCreateAccount={() =>
setScreenState(ScreenState.CreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.Signin ? (
setScreenState(ScreenState.SigninOrCreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.CreateAccount ? (
setScreenState(ScreenState.SigninOrCreateAccount)
}
/>
) : undefined}
)
},
)
const styles = StyleSheet.create({
outer: {
flex: 1,
},
hero: {
flex: 2,
justifyContent: 'center',
},
logoHero: {
paddingTop: 30,
paddingBottom: 40,
},
logo: {
flexDirection: 'row',
justifyContent: 'center',
},
title: {
textAlign: 'center',
color: colors.white,
fontSize: 68,
fontWeight: 'bold',
},
subtitle: {
textAlign: 'center',
color: colors.white,
fontSize: 18,
},
btn: {
borderWidth: 1,
borderColor: colors.white,
borderRadius: 10,
paddingVertical: 16,
marginBottom: 20,
marginHorizontal: 20,
backgroundColor: colors.blue3,
},
btnLabel: {
textAlign: 'center',
color: colors.white,
fontSize: 18,
fontWeight: 'bold',
},
or: {
marginBottom: 20,
},
orLine: {
position: 'absolute',
top: 10,
},
orLabel: {
textAlign: 'center',
color: colors.white,
fontSize: 16,
},
group: {
borderWidth: 1,
borderColor: colors.white,
borderRadius: 10,
marginBottom: 20,
marginHorizontal: 20,
backgroundColor: colors.blue3,
},
groupTitle: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 12,
},
groupTitleIcon: {
color: colors.white,
marginHorizontal: 6,
},
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,
},
textBtn: {
flexDirection: 'row',
flex: 1,
alignItems: 'center',
},
textBtnLabel: {
flex: 1,
color: colors.white,
paddingVertical: 10,
paddingHorizontal: 12,
fontSize: 18,
},
textBtnIcon: {
color: colors.white,
marginHorizontal: 12,
},
picker: {
flex: 1,
width: '100%',
backgroundColor: colors.blue3,
color: colors.white,
paddingVertical: 10,
paddingHorizontal: 12,
fontSize: 18,
borderRadius: 10,
},
pickerLabel: {
color: colors.white,
fontSize: 18,
},
pickerIcon: {
color: colors.white,
},
error: {
borderTopWidth: 1,
borderTopColor: colors.blue1,
flexDirection: 'row',
alignItems: 'center',
marginTop: 5,
backgroundColor: colors.blue2,
paddingHorizontal: 8,
paddingVertical: 5,
},
errorFloating: {
borderWidth: 1,
borderColor: colors.blue1,
marginBottom: 20,
marginHorizontal: 20,
borderRadius: 8,
},
errorIcon: {
borderWidth: 1,
borderColor: colors.white,
color: colors.white,
borderRadius: 30,
width: 16,
height: 16,
alignItems: 'center',
justifyContent: 'center',
marginRight: 5,
},
})