about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-12-15 14:06:05 -0600
committerPaul Frazee <pfrazee@gmail.com>2022-12-15 14:06:05 -0600
commit0d54f6e126276c5ced0b8dd0c67b3d2524f99b96 (patch)
tree0e7b45502e5ff725dc0fd8d27e7df548210c9324 /src
parent356870ef60a7d4e7d3a605ee8d56b1994ac020a1 (diff)
downloadvoidsky-0d54f6e126276c5ced0b8dd0c67b3d2524f99b96.tar.zst
Split login screen into component files
Diffstat (limited to 'src')
-rw-r--r--src/view/com/login/CreateAccount.tsx443
-rw-r--r--src/view/com/login/Logo.tsx42
-rw-r--r--src/view/com/login/Signin.tsx262
-rw-r--r--src/view/screens/Login.tsx646
4 files changed, 752 insertions, 641 deletions
diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx
new file mode 100644
index 000000000..37dcba2fd
--- /dev/null
+++ b/src/view/com/login/CreateAccount.tsx
@@ -0,0 +1,443 @@
+import React, {useState, useEffect} from 'react'
+import {
+  ActivityIndicator,
+  KeyboardAvoidingView,
+  ScrollView,
+  StyleSheet,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import * as EmailValidator from 'email-validator'
+import {Logo} from './Logo'
+import {Picker} from '../util/Picker'
+import {TextLink} from '../util/Link'
+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 {ServerInputModal} from '../../../state/models/shell-ui'
+import {ComAtprotoAccountCreate} from '../../../third-party/api/index'
+
+export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
+  const store = useStores()
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
+  const [error, setError] = useState<string>('')
+  const [serviceDescription, setServiceDescription] = useState<
+    ServiceDescription | undefined
+  >(undefined)
+  const [userDomain, setUserDomain] = useState<string>('')
+  const [inviteCode, setInviteCode] = useState<string>('')
+  const [email, setEmail] = useState<string>('')
+  const [password, setPassword] = useState<string>('')
+  const [handle, setHandle] = useState<string>('')
+
+  useEffect(() => {
+    let aborted = false
+    setError('')
+    setServiceDescription(undefined)
+    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, store.session])
+
+  const onPressSelectService = () => {
+    store.shell.openModal(new ServerInputModal(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 Policies = () => {
+    if (!serviceDescription) {
+      return <View />
+    }
+    const tos = validWebLink(serviceDescription.links?.termsOfService)
+    const pp = validWebLink(serviceDescription.links?.privacyPolicy)
+    if (!tos && !pp) {
+      return (
+        <View style={styles.policies}>
+          <View style={[styles.errorIcon, s.mt2]}>
+            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
+          </View>
+          <Text style={[s.white, s.pl5, s.flex1]}>
+            This service has not provided terms of service or a privacy policy.
+          </Text>
+        </View>
+      )
+    }
+    const els = []
+    if (tos) {
+      els.push(
+        <TextLink
+          href={tos}
+          text="Terms of Service"
+          style={[s.white, s.underline]}
+        />,
+      )
+    }
+    if (pp) {
+      els.push(
+        <TextLink
+          href={pp}
+          text="Privacy Policy"
+          style={[s.white, s.underline]}
+        />,
+      )
+    }
+    if (els.length === 2) {
+      els.splice(1, 0, <Text style={s.white}> and </Text>)
+    }
+    return (
+      <View style={styles.policies}>
+        <Text style={s.white}>
+          By creating an account you agree to the {els}.
+        </Text>
+      </View>
+    )
+  }
+
+  return (
+    <ScrollView style={{flex: 1}}>
+      <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
+        <View style={styles.logoHero}>
+          <Logo />
+        </View>
+        {error ? (
+          <View style={[styles.error, styles.errorFloating]}>
+            <View style={styles.errorIcon}>
+              <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
+            </View>
+            <View style={s.flex1}>
+              <Text style={[s.white, s.bold]}>{error}</Text>
+            </View>
+          </View>
+        ) : undefined}
+        <View style={[styles.group]}>
+          <View style={styles.groupTitle}>
+            <Text style={[s.white, s.f18, s.bold]}>Create a new account</Text>
+          </View>
+          <View style={styles.groupContent}>
+            <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
+            <TouchableOpacity
+              style={styles.textBtn}
+              onPress={onPressSelectService}>
+              <Text style={styles.textBtnLabel}>
+                {toNiceDomain(serviceUrl)}
+              </Text>
+              <View style={styles.textBtnFakeInnerBtn}>
+                <FontAwesomeIcon
+                  icon="pen"
+                  size={12}
+                  style={styles.textBtnFakeInnerBtnIcon}
+                />
+                <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
+              </View>
+            </TouchableOpacity>
+          </View>
+          {serviceDescription ? (
+            <>
+              {serviceDescription?.inviteCodeRequired ? (
+                <View style={styles.groupContent}>
+                  <FontAwesomeIcon
+                    icon="ticket"
+                    style={styles.groupContentIcon}
+                  />
+                  <TextInput
+                    style={[styles.textInput]}
+                    placeholder="Invite code"
+                    placeholderTextColor={colors.blue0}
+                    autoCapitalize="none"
+                    autoCorrect={false}
+                    autoFocus
+                    value={inviteCode}
+                    onChangeText={setInviteCode}
+                    editable={!isProcessing}
+                  />
+                </View>
+              ) : undefined}
+              <View style={styles.groupContent}>
+                <FontAwesomeIcon
+                  icon="envelope"
+                  style={styles.groupContentIcon}
+                />
+                <TextInput
+                  style={[styles.textInput]}
+                  placeholder="Email address"
+                  placeholderTextColor={colors.blue0}
+                  autoCapitalize="none"
+                  autoCorrect={false}
+                  value={email}
+                  onChangeText={setEmail}
+                  editable={!isProcessing}
+                />
+              </View>
+              <View style={styles.groupContent}>
+                <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
+                <TextInput
+                  style={[styles.textInput]}
+                  placeholder="Choose your password"
+                  placeholderTextColor={colors.blue0}
+                  autoCapitalize="none"
+                  autoCorrect={false}
+                  secureTextEntry
+                  value={password}
+                  onChangeText={setPassword}
+                  editable={!isProcessing}
+                />
+              </View>
+            </>
+          ) : undefined}
+        </View>
+        {serviceDescription ? (
+          <>
+            <View style={styles.group}>
+              <View style={styles.groupTitle}>
+                <Text style={[s.white, s.f18, s.bold]}>
+                  Choose your username
+                </Text>
+              </View>
+              <View style={styles.groupContent}>
+                <FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
+                <TextInput
+                  style={[styles.textInput]}
+                  placeholder="eg alice"
+                  placeholderTextColor={colors.blue0}
+                  autoCapitalize="none"
+                  value={handle}
+                  onChangeText={v => setHandle(makeValidHandle(v))}
+                  editable={!isProcessing}
+                />
+              </View>
+              {serviceDescription.availableUserDomains.length > 1 && (
+                <View style={styles.groupContent}>
+                  <FontAwesomeIcon
+                    icon="globe"
+                    style={styles.groupContentIcon}
+                  />
+                  <Picker
+                    style={styles.picker}
+                    labelStyle={styles.pickerLabel}
+                    iconStyle={styles.pickerIcon}
+                    value={userDomain}
+                    items={serviceDescription.availableUserDomains.map(d => ({
+                      label: `.${d}`,
+                      value: d,
+                    }))}
+                    onChange={itemValue => setUserDomain(itemValue)}
+                    enabled={!isProcessing}
+                  />
+                </View>
+              )}
+              <View style={styles.groupContent}>
+                <Text style={[s.white, s.p10]}>
+                  Your full username will be{' '}
+                  <Text style={s.bold}>
+                    @{createFullHandle(handle, userDomain)}
+                  </Text>
+                </Text>
+              </View>
+            </View>
+            <Policies />
+          </>
+        ) : undefined}
+        <View style={[s.flexRow, s.pl20, s.pr20, {paddingBottom: 200}]}>
+          <TouchableOpacity onPress={onPressBack}>
+            <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
+          </TouchableOpacity>
+          <View style={s.flex1} />
+          {serviceDescription ? (
+            <TouchableOpacity onPress={onPressNext}>
+              {isProcessing ? (
+                <ActivityIndicator color="#fff" />
+              ) : (
+                <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
+              )}
+            </TouchableOpacity>
+          ) : undefined}
+        </View>
+      </KeyboardAvoidingView>
+    </ScrollView>
+  )
+}
+
+function validWebLink(url?: string): string | undefined {
+  return url && (url.startsWith('http://') || url.startsWith('https://'))
+    ? url
+    : undefined
+}
+
+const styles = StyleSheet.create({
+  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,
+  },
+  textBtn: {
+    flexDirection: 'row',
+    flex: 1,
+    alignItems: 'center',
+  },
+  textBtnLabel: {
+    flex: 1,
+    color: colors.white,
+    paddingVertical: 10,
+    paddingHorizontal: 12,
+    fontSize: 18,
+  },
+  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,
+  },
+  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,
+  },
+  policies: {
+    flexDirection: 'row',
+    alignItems: 'flex-start',
+    paddingHorizontal: 20,
+    paddingBottom: 20,
+  },
+  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,
+  },
+  errorFloating: {
+    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,
+  },
+})
diff --git a/src/view/com/login/Logo.tsx b/src/view/com/login/Logo.tsx
new file mode 100644
index 000000000..d1dc9c671
--- /dev/null
+++ b/src/view/com/login/Logo.tsx
@@ -0,0 +1,42 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import Svg, {Circle, Line, Text as SvgText} from 'react-native-svg'
+
+export const Logo = () => {
+  return (
+    <View style={styles.logo}>
+      <Svg width="100" height="100">
+        <Circle
+          cx="50"
+          cy="50"
+          r="46"
+          fill="none"
+          stroke="white"
+          strokeWidth={2}
+        />
+        <Line stroke="white" strokeWidth={1} x1="30" x2="30" y1="0" y2="100" />
+        <Line stroke="white" strokeWidth={1} x1="74" x2="74" y1="0" y2="100" />
+        <Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="22" y2="22" />
+        <Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="74" y2="74" />
+        <SvgText
+          fill="none"
+          stroke="white"
+          strokeWidth={2}
+          fontSize="60"
+          fontWeight="bold"
+          x="52"
+          y="70"
+          textAnchor="middle">
+          B
+        </SvgText>
+      </Svg>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  logo: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+  },
+})
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
new file mode 100644
index 000000000..f11a4c6ca
--- /dev/null
+++ b/src/view/com/login/Signin.tsx
@@ -0,0 +1,262 @@
+import React, {useState, useEffect} from 'react'
+import {
+  ActivityIndicator,
+  KeyboardAvoidingView,
+  StyleSheet,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Logo} from './Logo'
+import {s, colors} from '../../lib/styles'
+import {createFullHandle, toNiceDomain} from '../../../lib/strings'
+import {useStores, DEFAULT_SERVICE} from '../../../state'
+import {ServiceDescription} from '../../../state/models/session'
+import {ServerInputModal} from '../../../state/models/shell-ui'
+import {isNetworkError} from '../../../lib/errors'
+
+export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
+  const store = useStores()
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
+  const [serviceDescription, setServiceDescription] = useState<
+    ServiceDescription | undefined
+  >(undefined)
+  const [error, setError] = useState<string>('')
+  const [handle, setHandle] = useState<string>('')
+  const [password, setPassword] = useState<string>('')
+
+  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
+    }
+  }, [store.session, serviceUrl])
+
+  const onPressSelectService = () => {
+    store.shell.openModal(new ServerInputModal(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 (isNetworkError(e)) {
+        setError(
+          'Unable to contact your service. Please check your Internet connection.',
+        )
+      } else {
+        setError(errMsg.replace(/^Error:/, ''))
+      }
+    }
+  }
+
+  return (
+    <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
+      <View style={styles.logoHero}>
+        <Logo />
+      </View>
+      <View style={styles.group}>
+        <TouchableOpacity
+          style={[styles.groupTitle, {paddingRight: 0, paddingVertical: 6}]}
+          onPress={onPressSelectService}>
+          <Text style={[s.flex1, s.white, s.f18, s.bold]} numberOfLines={1}>
+            Sign in to {toNiceDomain(serviceUrl)}
+          </Text>
+          <View style={styles.textBtnFakeInnerBtn}>
+            <FontAwesomeIcon
+              icon="pen"
+              size={12}
+              style={styles.textBtnFakeInnerBtnIcon}
+            />
+            <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
+          </View>
+        </TouchableOpacity>
+        <View style={styles.groupContent}>
+          <FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
+          <TextInput
+            style={styles.textInput}
+            placeholder="Username"
+            placeholderTextColor={colors.blue0}
+            autoCapitalize="none"
+            autoFocus
+            autoCorrect={false}
+            value={handle}
+            onChangeText={str => setHandle((str || '').toLowerCase())}
+            editable={!isProcessing}
+          />
+        </View>
+        <View style={styles.groupContent}>
+          <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
+          <TextInput
+            style={styles.textInput}
+            placeholder="Password"
+            placeholderTextColor={colors.blue0}
+            autoCapitalize="none"
+            autoCorrect={false}
+            secureTextEntry
+            value={password}
+            onChangeText={setPassword}
+            editable={!isProcessing}
+          />
+        </View>
+      </View>
+      {error ? (
+        <View style={styles.error}>
+          <View style={styles.errorIcon}>
+            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
+          </View>
+          <View style={s.flex1}>
+            <Text style={[s.white, s.bold]}>{error}</Text>
+          </View>
+        </View>
+      ) : undefined}
+      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
+        <TouchableOpacity onPress={onPressBack}>
+          <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
+        </TouchableOpacity>
+        <View style={s.flex1} />
+        <TouchableOpacity onPress={onPressNext}>
+          {!serviceDescription || isProcessing ? (
+            <ActivityIndicator color="#fff" />
+          ) : (
+            <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
+          )}
+        </TouchableOpacity>
+        {!serviceDescription || isProcessing ? (
+          <Text style={[s.white, s.f18, s.pl10]}>Connecting...</Text>
+        ) : undefined}
+      </View>
+    </KeyboardAvoidingView>
+  )
+}
+
+const styles = StyleSheet.create({
+  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,
+  },
+  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,
+  },
+})
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
index acf7e7199..0bb672b3b 100644
--- a/src/view/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -1,32 +1,17 @@
-import React, {useState, useEffect} from 'react'
+import React, {useState} 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 Svg, {Line} from 'react-native-svg'
 import {observer} from 'mobx-react-lite'
-import {Picker} from '../com/util/Picker'
-import {TextLink} from '../com/util/Link'
+import {Signin} from '../com/login/Signin'
+import {Logo} from '../com/login/Logo'
+import {CreateAccount} from '../com/login/CreateAccount'
 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 {ServerInputModal} from '../../state/models/shell-ui'
-import {ComAtprotoAccountCreate} from '../../third-party/api/index'
-import {isNetworkError} from '../../lib/errors'
 
 enum ScreenState {
   SigninOrCreateAccount,
@@ -34,38 +19,6 @@ enum ScreenState {
   CreateAccount,
 }
 
-const Logo = () => {
-  return (
-    <View style={styles.logo}>
-      <Svg width="100" height="100">
-        <Circle
-          cx="50"
-          cy="50"
-          r="46"
-          fill="none"
-          stroke="white"
-          strokeWidth={2}
-        />
-        <Line stroke="white" strokeWidth={1} x1="30" x2="30" y1="0" y2="100" />
-        <Line stroke="white" strokeWidth={1} x1="74" x2="74" y1="0" y2="100" />
-        <Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="22" y2="22" />
-        <Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="74" y2="74" />
-        <SvgText
-          fill="none"
-          stroke="white"
-          strokeWidth={2}
-          fontSize="60"
-          fontWeight="bold"
-          x="52"
-          y="70"
-          textAnchor="middle">
-          B
-        </SvgText>
-      </Svg>
-    </View>
-  )
-}
-
 const SigninOrCreateAccount = ({
   onPressSignin,
   onPressCreateAccount,
@@ -115,459 +68,6 @@ const SigninOrCreateAccount = ({
   )
 }
 
-const Signin = ({onPressBack}: {onPressBack: () => void}) => {
-  const store = useStores()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
-  const [serviceDescription, setServiceDescription] = useState<
-    ServiceDescription | undefined
-  >(undefined)
-  const [error, setError] = useState<string>('')
-  const [handle, setHandle] = useState<string>('')
-  const [password, setPassword] = useState<string>('')
-
-  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 ServerInputModal(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 (isNetworkError(e)) {
-        setError(
-          'Unable to contact your service. Please check your Internet connection.',
-        )
-      } else {
-        setError(errMsg.replace(/^Error:/, ''))
-      }
-    }
-  }
-
-  return (
-    <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
-      <View style={styles.logoHero}>
-        <Logo />
-      </View>
-      <View style={styles.group}>
-        <TouchableOpacity
-          style={[styles.groupTitle, {paddingRight: 0, paddingVertical: 6}]}
-          onPress={onPressSelectService}>
-          <Text style={[s.flex1, s.white, s.f18, s.bold]} numberOfLines={1}>
-            Sign in to {toNiceDomain(serviceUrl)}
-          </Text>
-          <View style={styles.textBtnFakeInnerBtn}>
-            <FontAwesomeIcon
-              icon="pen"
-              size={12}
-              style={styles.textBtnFakeInnerBtnIcon}
-            />
-            <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
-          </View>
-        </TouchableOpacity>
-        <View style={styles.groupContent}>
-          <FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
-          <TextInput
-            style={styles.textInput}
-            placeholder="Username"
-            placeholderTextColor={colors.blue0}
-            autoCapitalize="none"
-            autoFocus
-            autoCorrect={false}
-            value={handle}
-            onChangeText={str => setHandle((str || '').toLowerCase())}
-            editable={!isProcessing}
-          />
-        </View>
-        <View style={styles.groupContent}>
-          <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
-          <TextInput
-            style={styles.textInput}
-            placeholder="Password"
-            placeholderTextColor={colors.blue0}
-            autoCapitalize="none"
-            autoCorrect={false}
-            secureTextEntry
-            value={password}
-            onChangeText={setPassword}
-            editable={!isProcessing}
-          />
-        </View>
-      </View>
-      {error ? (
-        <View style={styles.error}>
-          <View style={styles.errorIcon}>
-            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-          </View>
-          <View style={s.flex1}>
-            <Text style={[s.white, s.bold]}>{error}</Text>
-          </View>
-        </View>
-      ) : undefined}
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <TouchableOpacity onPress={onPressBack}>
-          <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-        <TouchableOpacity onPress={onPressNext}>
-          {!serviceDescription || isProcessing ? (
-            <ActivityIndicator color="#fff" />
-          ) : (
-            <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
-          )}
-        </TouchableOpacity>
-        {!serviceDescription || isProcessing ? (
-          <Text style={[s.white, s.f18, s.pl10]}>Connecting...</Text>
-        ) : undefined}
-      </View>
-    </KeyboardAvoidingView>
-  )
-}
-
-const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
-  const store = useStores()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
-  const [error, setError] = useState<string>('')
-  const [serviceDescription, setServiceDescription] = useState<
-    ServiceDescription | undefined
-  >(undefined)
-  const [userDomain, setUserDomain] = useState<string>('')
-  const [inviteCode, setInviteCode] = useState<string>('')
-  const [email, setEmail] = useState<string>('')
-  const [password, setPassword] = useState<string>('')
-  const [handle, setHandle] = useState<string>('')
-
-  useEffect(() => {
-    let aborted = false
-    setError('')
-    setServiceDescription(undefined)
-    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 ServerInputModal(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 Policies = () => {
-    if (!serviceDescription) {
-      return <View />
-    }
-    const tos = validWebLink(serviceDescription.links?.termsOfService)
-    const pp = validWebLink(serviceDescription.links?.privacyPolicy)
-    if (!tos && !pp) {
-      return (
-        <View style={styles.policies}>
-          <View style={[styles.errorIcon, s.mt2]}>
-            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-          </View>
-          <Text style={[s.white, s.pl5, s.flex1]}>
-            This service has not provided terms of service or a privacy policy.
-          </Text>
-        </View>
-      )
-    }
-    const els = []
-    if (tos) {
-      els.push(
-        <TextLink
-          href={tos}
-          text="Terms of Service"
-          style={[s.white, s.underline]}
-        />,
-      )
-    }
-    if (pp) {
-      els.push(
-        <TextLink
-          href={pp}
-          text="Privacy Policy"
-          style={[s.white, s.underline]}
-        />,
-      )
-    }
-    if (els.length === 2) {
-      els.splice(1, 0, <Text style={s.white}> and </Text>)
-    }
-    return (
-      <View style={styles.policies}>
-        <Text style={s.white}>
-          By creating an account you agree to the {els}.
-        </Text>
-      </View>
-    )
-  }
-
-  return (
-    <ScrollView style={{flex: 1}}>
-      <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
-        <View style={styles.logoHero}>
-          <Logo />
-        </View>
-        {error ? (
-          <View style={[styles.error, styles.errorFloating]}>
-            <View style={styles.errorIcon}>
-              <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-            </View>
-            <View style={s.flex1}>
-              <Text style={[s.white, s.bold]}>{error}</Text>
-            </View>
-          </View>
-        ) : undefined}
-        <View style={[styles.group]}>
-          <View style={styles.groupTitle}>
-            <Text style={[s.white, s.f18, s.bold]}>Create a new account</Text>
-          </View>
-          <View style={styles.groupContent}>
-            <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
-            <TouchableOpacity
-              style={styles.textBtn}
-              onPress={onPressSelectService}>
-              <Text style={styles.textBtnLabel}>
-                {toNiceDomain(serviceUrl)}
-              </Text>
-              <View style={styles.textBtnFakeInnerBtn}>
-                <FontAwesomeIcon
-                  icon="pen"
-                  size={12}
-                  style={styles.textBtnFakeInnerBtnIcon}
-                />
-                <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
-              </View>
-            </TouchableOpacity>
-          </View>
-          {serviceDescription ? (
-            <>
-              {serviceDescription?.inviteCodeRequired ? (
-                <View style={styles.groupContent}>
-                  <FontAwesomeIcon
-                    icon="ticket"
-                    style={styles.groupContentIcon}
-                  />
-                  <TextInput
-                    style={[styles.textInput]}
-                    placeholder="Invite code"
-                    placeholderTextColor={colors.blue0}
-                    autoCapitalize="none"
-                    autoCorrect={false}
-                    autoFocus
-                    value={inviteCode}
-                    onChangeText={setInviteCode}
-                    editable={!isProcessing}
-                  />
-                </View>
-              ) : undefined}
-              <View style={styles.groupContent}>
-                <FontAwesomeIcon
-                  icon="envelope"
-                  style={styles.groupContentIcon}
-                />
-                <TextInput
-                  style={[styles.textInput]}
-                  placeholder="Email address"
-                  placeholderTextColor={colors.blue0}
-                  autoCapitalize="none"
-                  autoCorrect={false}
-                  value={email}
-                  onChangeText={setEmail}
-                  editable={!isProcessing}
-                />
-              </View>
-              <View style={styles.groupContent}>
-                <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
-                <TextInput
-                  style={[styles.textInput]}
-                  placeholder="Choose your password"
-                  placeholderTextColor={colors.blue0}
-                  autoCapitalize="none"
-                  autoCorrect={false}
-                  secureTextEntry
-                  value={password}
-                  onChangeText={setPassword}
-                  editable={!isProcessing}
-                />
-              </View>
-            </>
-          ) : undefined}
-        </View>
-        {serviceDescription ? (
-          <>
-            <View style={styles.group}>
-              <View style={styles.groupTitle}>
-                <Text style={[s.white, s.f18, s.bold]}>
-                  Choose your username
-                </Text>
-              </View>
-              <View style={styles.groupContent}>
-                <FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
-                <TextInput
-                  style={[styles.textInput]}
-                  placeholder="eg alice"
-                  placeholderTextColor={colors.blue0}
-                  autoCapitalize="none"
-                  value={handle}
-                  onChangeText={v => setHandle(makeValidHandle(v))}
-                  editable={!isProcessing}
-                />
-              </View>
-              {serviceDescription.availableUserDomains.length > 1 && (
-                <View style={styles.groupContent}>
-                  <FontAwesomeIcon
-                    icon="globe"
-                    style={styles.groupContentIcon}
-                  />
-                  <Picker
-                    style={styles.picker}
-                    labelStyle={styles.pickerLabel}
-                    iconStyle={styles.pickerIcon}
-                    value={userDomain}
-                    items={serviceDescription.availableUserDomains.map(d => ({
-                      label: `.${d}`,
-                      value: d,
-                    }))}
-                    onChange={itemValue => setUserDomain(itemValue)}
-                    enabled={!isProcessing}
-                  />
-                </View>
-              )}
-              <View style={styles.groupContent}>
-                <Text style={[s.white, s.p10]}>
-                  Your full username will be{' '}
-                  <Text style={s.bold}>
-                    @{createFullHandle(handle, userDomain)}
-                  </Text>
-                </Text>
-              </View>
-            </View>
-            <Policies />
-          </>
-        ) : undefined}
-        <View style={[s.flexRow, s.pl20, s.pr20, {paddingBottom: 200}]}>
-          <TouchableOpacity onPress={onPressBack}>
-            <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {serviceDescription ? (
-            <TouchableOpacity onPress={onPressNext}>
-              {isProcessing ? (
-                <ActivityIndicator color="#fff" />
-              ) : (
-                <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
-              )}
-            </TouchableOpacity>
-          ) : undefined}
-        </View>
-      </KeyboardAvoidingView>
-    </ScrollView>
-  )
-}
-
 export const Login = observer(
   (/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
     const [screenState, setScreenState] = useState<ScreenState>(
@@ -603,12 +103,6 @@ export const Login = observer(
   },
 )
 
-function validWebLink(url?: string): string | undefined {
-  return url && (url.startsWith('http://') || url.startsWith('https://'))
-    ? url
-    : undefined
-}
-
 const styles = StyleSheet.create({
   outer: {
     flex: 1,
@@ -617,14 +111,6 @@ const styles = StyleSheet.create({
     flex: 2,
     justifyContent: 'center',
   },
-  logoHero: {
-    paddingTop: 30,
-    paddingBottom: 40,
-  },
-  logo: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-  },
   title: {
     textAlign: 'center',
     color: colors.white,
@@ -663,126 +149,4 @@ const styles = StyleSheet.create({
     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,
-  },
-  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,
-  },
-  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,
-  },
-  policies: {
-    flexDirection: 'row',
-    alignItems: 'flex-start',
-    paddingHorizontal: 20,
-    paddingBottom: 20,
-  },
-  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,
-  },
-  errorFloating: {
-    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,
-  },
 })