import React from 'react' import { ActivityIndicator, Keyboard, KeyboardAvoidingView, ScrollView, StyleSheet, TextInput, TouchableOpacity, View, } from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {ComAtprotoAccountCreate} from '@atproto/api' import * as EmailValidator from 'email-validator' import {sha256} from 'js-sha256' import {useAnalytics} from 'lib/analytics' import {LogoTextHero} from './Logo' import {Picker} from '../util/Picker' import {TextLink} from '../util/Link' import {Text} from '../util/text/Text' import {s, colors} from 'lib/styles' import {makeValidHandle, createFullHandle} from 'lib/strings/handles' import {toNiceDomain} from 'lib/strings/url-helpers' import {useStores, DEFAULT_SERVICE} from 'state/index' import {ServiceDescription} from 'state/models/session' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {cleanError} from 'lib/strings/errors' export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { const {track, screen, identify} = useAnalytics() const pal = usePalette('default') const theme = useTheme() const store = useStores() const [isProcessing, setIsProcessing] = React.useState(false) const [serviceUrl, setServiceUrl] = React.useState(DEFAULT_SERVICE) const [error, setError] = React.useState('') const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState( {}, ) const [serviceDescription, setServiceDescription] = React.useState< ServiceDescription | undefined >(undefined) const [userDomain, setUserDomain] = React.useState('') const [inviteCode, setInviteCode] = React.useState('') const [email, setEmail] = React.useState('') const [password, setPassword] = React.useState('') const [handle, setHandle] = React.useState('') const [is13, setIs13] = React.useState(false) React.useEffect(() => { screen('CreateAccount') }, [screen]) React.useEffect(() => { let aborted = false setError('') setServiceDescription(undefined) store.session.describeService(serviceUrl).then( desc => { if (aborted) { return } setServiceDescription(desc) setUserDomain(desc.availableUserDomains[0]) }, err => { if (aborted) { return } store.log.warn( `Failed to fetch service description for ${serviceUrl}`, err, ) setError( 'Unable to contact your service. Please check your Internet connection.', ) }, ) return () => { aborted = true } }, [serviceUrl, store.session, store.log, retryDescribeTrigger]) const onPressRetryConnect = React.useCallback( () => setRetryDescribeTrigger({}), [setRetryDescribeTrigger], ) const onPressSelectService = React.useCallback(() => { store.shell.openModal({ name: 'server-input', initialService: serviceUrl, onSelect: setServiceUrl, }) Keyboard.dismiss() }, [store, serviceUrl]) const onBlurInviteCode = React.useCallback(() => { setInviteCode(inviteCode.trim()) }, [setInviteCode, inviteCode]) const onPressNext = React.useCallback(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, }) const email_hashed = sha256(email) identify(email_hashed, {email_hashed}) track('Create Account') } 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.' } store.log.error('Failed to create account', e) setIsProcessing(false) setError(cleanError(errMsg)) } }, [ serviceUrl, userDomain, inviteCode, email, password, handle, setError, setIsProcessing, store, track, identify, ]) const isReady = !!email && !!password && !!handle && is13 return ( {error ? ( {error} ) : undefined} Service provider {toNiceDomain(serviceUrl)} Change {serviceDescription ? ( <> Account details {serviceDescription?.inviteCodeRequired ? ( ) : undefined} ) : undefined} {serviceDescription ? ( <> 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)} Legal setIs13(!is13)}> {is13 && ( )} I am 13 years old or older ) : undefined} Back {isReady ? ( {isProcessing ? ( ) : ( Next )} ) : !serviceDescription && error ? ( Retry ) : !serviceDescription ? ( <> Connecting... ) : undefined} ) } const Policies = ({ serviceDescription, }: { serviceDescription: ServiceDescription }) => { const pal = usePalette('default') if (!serviceDescription) { return } const tos = validWebLink(serviceDescription.links?.termsOfService) const pp = validWebLink(serviceDescription.links?.privacyPolicy) if (!tos && !pp) { return ( This service has not provided terms of service or a privacy policy. ) } const els = [] if (tos) { els.push( , ) } if (pp) { els.push( , ) } if (els.length === 2) { els.splice( 1, 0, {' '} and{' '} , ) } return ( By creating an account you agree to the {els}. ) } function validWebLink(url?: string): string | undefined { return url && (url.startsWith('http://') || url.startsWith('https://')) ? url : undefined } const styles = StyleSheet.create({ noTopBorder: { borderTopWidth: 0, }, logoHero: { paddingTop: 30, paddingBottom: 40, }, group: { borderWidth: 1, borderRadius: 10, marginBottom: 20, marginHorizontal: 20, }, groupLabel: { paddingHorizontal: 20, paddingBottom: 5, }, groupContent: { borderTopWidth: 1, flexDirection: 'row', alignItems: 'center', }, groupContentIcon: { marginLeft: 10, }, textInput: { flex: 1, width: '100%', paddingVertical: 10, paddingHorizontal: 12, fontSize: 17, letterSpacing: 0.25, fontWeight: '400', borderRadius: 10, }, textBtn: { flexDirection: 'row', flex: 1, alignItems: 'center', }, textBtnLabel: { flex: 1, paddingVertical: 10, paddingHorizontal: 12, }, textBtnFakeInnerBtn: { flexDirection: 'row', alignItems: 'center', borderRadius: 6, paddingVertical: 6, paddingHorizontal: 8, marginHorizontal: 6, }, textBtnFakeInnerBtnIcon: { marginRight: 4, }, picker: { flex: 1, width: '100%', paddingVertical: 10, paddingHorizontal: 12, fontSize: 17, borderRadius: 10, }, pickerLabel: { fontSize: 17, }, checkbox: { borderWidth: 1, borderRadius: 2, width: 16, height: 16, marginLeft: 16, }, checkboxFilled: { borderWidth: 1, borderRadius: 2, width: 16, height: 16, marginLeft: 16, }, policies: { flexDirection: 'row', alignItems: 'flex-start', paddingHorizontal: 20, paddingBottom: 20, }, error: { backgroundColor: colors.red4, flexDirection: 'row', alignItems: 'center', marginTop: -5, marginHorizontal: 20, marginBottom: 15, borderRadius: 8, paddingHorizontal: 8, paddingVertical: 8, }, errorFloating: { marginBottom: 20, marginHorizontal: 20, borderRadius: 8, }, errorIcon: { borderWidth: 1, borderColor: colors.white, borderRadius: 30, width: 16, height: 16, alignItems: 'center', justifyContent: 'center', marginRight: 5, }, })