about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-11-15 15:09:50 -0600
committerPaul Frazee <pfrazee@gmail.com>2022-11-15 15:09:50 -0600
commit3725a2eed10707194bc9554a9c58166e9324dfc8 (patch)
treef167b4541f86eb5b9ff8629c0bbb6486644f498b
parent9a6df95adecaf3935fdbd58d893fca6489a040b9 (diff)
downloadvoidsky-3725a2eed10707194bc9554a9c58166e9324dfc8.tar.zst
Add a server instance selector and drop env vars
-rw-r--r--.env.development1
-rw-r--r--.env.production1
-rw-r--r--src/env.native.ts13
-rw-r--r--src/env.ts10
-rw-r--r--src/state/index.ts14
-rw-r--r--src/state/models/shell-ui.ts15
-rw-r--r--src/view/com/modals/Modal.tsx8
-rw-r--r--src/view/com/modals/ServerInput.tsx140
-rw-r--r--src/view/index.ts4
-rw-r--r--src/view/lib/strings.ts13
-rw-r--r--src/view/screens/Home.tsx6
-rw-r--r--src/view/screens/Login.tsx322
-rw-r--r--src/view/shell/mobile/index.tsx2
-rw-r--r--todos.txt8
14 files changed, 383 insertions, 174 deletions
diff --git a/.env.development b/.env.development
deleted file mode 100644
index d0f279676..000000000
--- a/.env.development
+++ /dev/null
@@ -1 +0,0 @@
-REACT_APP_BUILD = 'staging'
diff --git a/.env.production b/.env.production
deleted file mode 100644
index d0f279676..000000000
--- a/.env.production
+++ /dev/null
@@ -1 +0,0 @@
-REACT_APP_BUILD = 'staging'
diff --git a/src/env.native.ts b/src/env.native.ts
deleted file mode 100644
index a2ec3a4dc..000000000
--- a/src/env.native.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// @ts-ignore types not available -prf
-import {REACT_APP_BUILD} from '@env'
-
-if (typeof REACT_APP_BUILD !== 'string') {
-  throw new Error('ENV: No env provided')
-}
-if (!['dev', 'staging', 'prod'].includes(REACT_APP_BUILD)) {
-  throw new Error(
-    `ENV: Env must be "dev", "staging", or "prod," got "${REACT_APP_BUILD}"`,
-  )
-}
-
-export const BUILD = REACT_APP_BUILD
diff --git a/src/env.ts b/src/env.ts
deleted file mode 100644
index a379e435f..000000000
--- a/src/env.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-if (typeof process.env.REACT_APP_BUILD !== 'string') {
-  throw new Error('ENV: No env provided')
-}
-if (!['dev', 'staging', 'prod'].includes(process.env.REACT_APP_BUILD)) {
-  throw new Error(
-    `ENV: Env must be "dev", "staging", or "prod," got "${process.env.REACT_APP_BUILD}"`,
-  )
-}
-
-export const BUILD = process.env.REACT_APP_BUILD
diff --git a/src/state/index.ts b/src/state/index.ts
index a886e7611..b16b51648 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -3,14 +3,12 @@ import {sessionClient as AtpApi} from '../third-party/api'
 import {RootStoreModel} from './models/root-store'
 import * as libapi from './lib/api'
 import * as storage from './lib/storage'
-import {BUILD} from '../env'
-
-export const DEFAULT_SERVICE =
-  BUILD === 'prod'
-    ? 'http://localhost:2583' // TODO
-    : BUILD === 'staging'
-    ? 'https://pds.staging.bsky.dev' // TODO
-    : 'http://localhost:2583'
+
+export const IS_PROD_BUILD = true
+export const LOCAL_DEV_SERVICE = 'http://localhost:2583'
+export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
+export const PROD_SERVICE = 'https://plc.bsky.social'
+export const DEFAULT_SERVICE = IS_PROD_BUILD ? PROD_SERVICE : LOCAL_DEV_SERVICE
 const ROOT_STATE_STORAGE_KEY = 'root'
 const STATE_FETCH_INTERVAL = 15e3
 
diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts
index cc884f1c3..73b1bd56e 100644
--- a/src/state/models/shell-ui.ts
+++ b/src/state/models/shell-ui.ts
@@ -66,6 +66,17 @@ export class InviteToSceneModel {
   }
 }
 
+export class ServerInputModel {
+  name = 'server-input'
+
+  constructor(
+    public initialService: string,
+    public onSelect: (url: string) => void,
+  ) {
+    makeAutoObservable(this)
+  }
+}
+
 export interface ComposerOpts {
   replyTo?: Post.PostRef
   onPost?: () => void
@@ -79,6 +90,7 @@ export class ShellUiModel {
     | SharePostModel
     | EditProfileModel
     | CreateSceneModel
+    | ServerInputModel
     | undefined
   isComposerActive = false
   composerOpts: ComposerOpts | undefined
@@ -93,7 +105,8 @@ export class ShellUiModel {
       | ConfirmModel
       | SharePostModel
       | EditProfileModel
-      | CreateSceneModel,
+      | CreateSceneModel
+      | ServerInputModel,
   ) {
     this.isModalActive = true
     this.activeModal = modal
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index f2c61a6ae..210cdc41f 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -13,6 +13,7 @@ import * as SharePostModal from './SharePost.native'
 import * as EditProfileModal from './EditProfile'
 import * as CreateSceneModal from './CreateScene'
 import * as InviteToSceneModal from './InviteToScene'
+import * as ServerInputModal from './ServerInput'
 
 const CLOSED_SNAPPOINTS = ['10%']
 
@@ -77,6 +78,13 @@ export const Modal = observer(function Modal() {
         {...(store.shell.activeModal as models.InviteToSceneModel)}
       />
     )
+  } else if (store.shell.activeModal?.name === 'server-input') {
+    snapPoints = ServerInputModal.snapPoints
+    element = (
+      <ServerInputModal.Component
+        {...(store.shell.activeModal as models.ServerInputModel)}
+      />
+    )
   } else {
     element = <View />
   }
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
new file mode 100644
index 000000000..1f3cc90f9
--- /dev/null
+++ b/src/view/com/modals/ServerInput.tsx
@@ -0,0 +1,140 @@
+import React, {useState} from 'react'
+import Toast from '../util/Toast'
+import {StyleSheet, Text, TextInput, TouchableOpacity, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import LinearGradient from 'react-native-linear-gradient'
+import {ErrorMessage} from '../util/ErrorMessage'
+import {useStores} from '../../../state'
+import {ProfileViewModel} from '../../../state/models/profile-view'
+import {s, colors, gradients} from '../../lib/styles'
+import {enforceLen, MAX_DISPLAY_NAME, MAX_DESCRIPTION} from '../../lib/strings'
+import {
+  IS_PROD_BUILD,
+  LOCAL_DEV_SERVICE,
+  STAGING_SERVICE,
+  PROD_SERVICE,
+} from '../../../state/index'
+
+export const snapPoints = ['80%']
+
+export function Component({
+  initialService,
+  onSelect,
+}: {
+  initialService: string
+  onSelect: (url: string) => void
+}) {
+  const store = useStores()
+  const [customUrl, setCustomUrl] = useState<string>('')
+
+  const doSelect = (url: string) => {
+    if (!url.startsWith('http://') && !url.startsWith('https://')) {
+      url = `https://${url}`
+    }
+    store.shell.closeModal()
+    onSelect(url)
+  }
+
+  return (
+    <View style={s.flex1}>
+      <Text style={[s.textCenter, s.bold, s.f18]}>Choose Service</Text>
+      <View style={styles.inner}>
+        <View style={styles.group}>
+          {!IS_PROD_BUILD ? (
+            <>
+              <TouchableOpacity
+                style={styles.btn}
+                onPress={() => doSelect(LOCAL_DEV_SERVICE)}>
+                <Text style={styles.btnText}>Local dev server</Text>
+                <FontAwesomeIcon icon="arrow-right" style={s.white} />
+              </TouchableOpacity>
+              <TouchableOpacity
+                style={styles.btn}
+                onPress={() => doSelect(STAGING_SERVICE)}>
+                <Text style={styles.btnText}>Staging</Text>
+                <FontAwesomeIcon icon="arrow-right" style={s.white} />
+              </TouchableOpacity>
+            </>
+          ) : undefined}
+          <TouchableOpacity
+            style={styles.btn}
+            onPress={() => doSelect(PROD_SERVICE)}>
+            <Text style={styles.btnText}>Bluesky.Social</Text>
+            <FontAwesomeIcon icon="arrow-right" style={s.white} />
+          </TouchableOpacity>
+        </View>
+        <View style={styles.group}>
+          <Text style={styles.label}>Other service</Text>
+          <View style={{flexDirection: 'row'}}>
+            <TextInput
+              style={styles.textInput}
+              placeholder="e.g. https://bsky.app"
+              autoCapitalize="none"
+              autoComplete="off"
+              autoCorrect={false}
+              value={customUrl}
+              onChangeText={setCustomUrl}
+            />
+            <TouchableOpacity
+              style={styles.textInputBtn}
+              onPress={() => doSelect(customUrl)}>
+              <FontAwesomeIcon
+                icon="check"
+                style={[s.black, {position: 'relative', top: 2}]}
+                size={18}
+              />
+            </TouchableOpacity>
+          </View>
+        </View>
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  inner: {
+    padding: 14,
+  },
+  group: {
+    marginBottom: 20,
+  },
+  label: {
+    fontWeight: 'bold',
+    paddingHorizontal: 4,
+    paddingBottom: 4,
+  },
+  textInput: {
+    flex: 1,
+    borderWidth: 1,
+    borderColor: colors.gray3,
+    borderTopLeftRadius: 6,
+    borderBottomLeftRadius: 6,
+    paddingHorizontal: 14,
+    paddingVertical: 12,
+    fontSize: 16,
+  },
+  textInputBtn: {
+    borderWidth: 1,
+    borderLeftWidth: 0,
+    borderColor: colors.gray3,
+    borderTopRightRadius: 6,
+    borderBottomRightRadius: 6,
+    paddingHorizontal: 14,
+    paddingVertical: 10,
+  },
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    backgroundColor: colors.blue3,
+    borderRadius: 6,
+    paddingHorizontal: 14,
+    paddingVertical: 10,
+    marginBottom: 6,
+  },
+  btnText: {
+    flex: 1,
+    fontSize: 18,
+    fontWeight: '500',
+    color: colors.white,
+  },
+})
diff --git a/src/view/index.ts b/src/view/index.ts
index 341051d4e..78361e75b 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -5,6 +5,7 @@ import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
 import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
 import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
 import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
+import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight'
 import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons'
 import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
 import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
@@ -35,6 +36,7 @@ import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
 import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
 import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage'
 import {faNoteSticky} from '@fortawesome/free-solid-svg-icons/faNoteSticky'
+import {faPen} from '@fortawesome/free-solid-svg-icons/faPen'
 import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
 import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare'
 import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
@@ -59,6 +61,7 @@ export function setup() {
     faAngleLeft,
     faAngleRight,
     faArrowLeft,
+    faArrowRight,
     faArrowRightFromBracket,
     faArrowUpFromBracket,
     faArrowUpRightFromSquare,
@@ -89,6 +92,7 @@ export function setup() {
     faMagnifyingGlass,
     faMessage,
     faNoteSticky,
+    faPen,
     faPenNib,
     faPenToSquare,
     faPlus,
diff --git a/src/view/lib/strings.ts b/src/view/lib/strings.ts
index 214bb51d6..05b23331f 100644
--- a/src/view/lib/strings.ts
+++ b/src/view/lib/strings.ts
@@ -1,5 +1,6 @@
 import {AtUri} from '../../third-party/uri'
 import {Entity} from '../../third-party/api/src/client/types/app/bsky/feed/post'
+import {PROD_SERVICE} from '../../state'
 
 export const MAX_DISPLAY_NAME = 64
 export const MAX_DESCRIPTION = 256
@@ -106,3 +107,15 @@ export function cleanError(str: string): string {
   }
   return str
 }
+
+export function toNiceDomain(url: string): string {
+  try {
+    const urlp = new URL(url)
+    if (`https://${urlp.host}` === PROD_SERVICE) {
+      return 'Bluesky.Social'
+    }
+    return urlp.host
+  } catch (e) {
+    return url
+  }
+}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 036f7d148..04ce2d0cb 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -8,7 +8,6 @@ import {useStores} from '../../state'
 import {FeedModel} from '../../state/models/feed-view'
 import {ScreenParams} from '../routes'
 import {s} from '../lib/styles'
-import {BUILD} from '../../env'
 
 export const Home = observer(function Home({
   visible,
@@ -57,10 +56,7 @@ export const Home = observer(function Home({
 
   return (
     <View style={s.flex1}>
-      <ViewHeader
-        title="Bluesky"
-        subtitle={`Private Beta${BUILD !== 'prod' ? ` [${BUILD}]` : ''}`}
-      />
+      <ViewHeader title="Bluesky" subtitle="Private Beta" />
       <Feed
         key="default"
         feed={defaultFeedView}
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
index ac93613eb..328a56e9a 100644
--- a/src/view/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -2,6 +2,7 @@ import React, {useState, useEffect} from 'react'
 import {
   ActivityIndicator,
   KeyboardAvoidingView,
+  ScrollView,
   StyleSheet,
   Text,
   TextInput,
@@ -15,10 +16,10 @@ 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} from '../lib/strings'
+import {makeValidHandle, createFullHandle, toNiceDomain} from '../lib/strings'
 import {useStores, DEFAULT_SERVICE} from '../../state'
 import {ServiceDescription} from '../../state/models/session'
-import {BUILD} from '../../env'
+import {ServerInputModel} from '../../state/models/shell-ui'
 
 enum ScreenState {
   SigninOrCreateAccount,
@@ -72,9 +73,7 @@ const SigninOrCreateAccount = ({
       <View style={styles.hero}>
         <Logo />
         <Text style={styles.title}>Bluesky</Text>
-        <Text style={styles.subtitle}>
-          [ private beta {BUILD !== 'prod' ? `- ${BUILD} ` : ''}]
-        </Text>
+        <Text style={styles.subtitle}>[ private beta ]</Text>
       </View>
       <View style={s.flex1}>
         <TouchableOpacity style={styles.btn} onPress={onPressCreateAccount}>
@@ -112,6 +111,7 @@ 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)
@@ -121,10 +121,9 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
 
   useEffect(() => {
     let aborted = false
-    if (serviceDescription || error) {
-      return
-    }
-    store.session.describeService(DEFAULT_SERVICE).then(
+    setError('')
+    console.log('Fetching service description', serviceUrl)
+    store.session.describeService(serviceUrl).then(
       desc => {
         if (aborted) return
         setServiceDescription(desc)
@@ -140,7 +139,11 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
     return () => {
       aborted = true
     }
-  }, [])
+  }, [serviceUrl])
+
+  const onPressSelectService = () => {
+    store.shell.openModal(new ServerInputModel(serviceUrl, setServiceUrl))
+  }
 
   const onPressNext = async () => {
     setError('')
@@ -168,7 +171,7 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
       }
 
       await store.session.login({
-        service: DEFAULT_SERVICE,
+        service: serviceUrl,
         handle: fullHandle,
         password,
       })
@@ -194,9 +197,14 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
         <Logo />
       </View>
       <View style={styles.group}>
-        <View style={styles.groupTitle}>
-          <Text style={[s.white, s.f18, s.bold]}>Sign in</Text>
-        </View>
+        <TouchableOpacity
+          style={styles.groupTitle}
+          onPress={onPressSelectService}>
+          <Text style={[s.white, s.f18, s.bold]} numberOfLines={1}>
+            Sign in to {toNiceDomain(serviceUrl)}
+          </Text>
+          <FontAwesomeIcon icon="pen" size={10} style={styles.groupTitleIcon} />
+        </TouchableOpacity>
         {error ? (
           <View style={styles.error}>
             <View style={styles.errorIcon}>
@@ -256,6 +264,7 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
 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
@@ -268,10 +277,9 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
 
   useEffect(() => {
     let aborted = false
-    if (serviceDescription || error) {
-      return
-    }
-    store.session.describeService(DEFAULT_SERVICE).then(
+    setError('')
+    console.log('Fetching service description', serviceUrl)
+    store.session.describeService(serviceUrl).then(
       desc => {
         if (aborted) return
         setServiceDescription(desc)
@@ -288,7 +296,11 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     return () => {
       aborted = true
     }
-  }, [])
+  }, [serviceUrl])
+
+  const onPressSelectService = () => {
+    store.shell.openModal(new ServerInputModel(serviceUrl, setServiceUrl))
+  }
 
   const onPressNext = async () => {
     if (!email) {
@@ -307,7 +319,7 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     setIsProcessing(true)
     try {
       await store.session.createAccount({
-        service: DEFAULT_SERVICE,
+        service: serviceUrl,
         email,
         handle: createFullHandle(handle, userDomain),
         password,
@@ -346,136 +358,164 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
   )
 
   return (
-    <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
-      <View style={styles.logoHero}>
-        <Logo />
-      </View>
-      {serviceDescription ? (
-        <>
-          {error ? (
-            <View style={[styles.error, styles.errorFloating]}>
-              <View style={styles.errorIcon}>
-                <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
+    <ScrollView style={{flex: 1}}>
+      <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
+        <View style={styles.logoHero}>
+          <Logo />
+        </View>
+        {serviceDescription ? (
+          <>
+            {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>
-              <View style={s.flex1}>
-                <Text style={[s.white, s.bold]}>{error}</Text>
+            ) : undefined}
+            <View style={[styles.group]}>
+              <View style={styles.groupTitle}>
+                <Text style={[s.white, s.f18, s.bold]}>
+                  Create a new account
+                </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>
-            {serviceDescription?.inviteCodeRequired ? (
+              <View style={styles.groupContent}>
+                <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
+                <TouchableOpacity
+                  style={styles.textBtn}
+                  onPress={onPressSelectService}>
+                  <Text style={styles.textBtnLabel}>
+                    {toNiceDomain(serviceUrl)}
+                  </Text>
+                  <FontAwesomeIcon
+                    icon="pen"
+                    size={12}
+                    style={styles.textBtnIcon}
+                  />
+                </TouchableOpacity>
+              </View>
+              {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="ticket"
+                  icon="envelope"
                   style={styles.groupContentIcon}
                 />
                 <TextInput
                   style={[styles.textInput]}
-                  placeholder="Invite code"
+                  placeholder="Email address"
                   placeholderTextColor={colors.blue0}
                   autoCapitalize="none"
                   autoCorrect={false}
-                  autoFocus
-                  value={inviteCode}
-                  onChangeText={setInviteCode}
+                  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 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>
-          </View>
-          <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.group}>
+              <View style={styles.groupTitle}>
+                <Text style={[s.white, s.f18, s.bold]}>
+                  Choose your username
+                </Text>
+              </View>
               <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}
+                <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>
-            )}
-            <View style={styles.groupContent}>
-              <Text style={[s.white, s.p10]}>
-                Your full username will be{' '}
-                <Text style={s.bold}>
-                  @{createFullHandle(handle, userDomain)}
+              {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>
-              </Text>
+              </View>
             </View>
-          </View>
-          <View style={[s.flexRow, 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}>
-              {isProcessing ? (
-                <ActivityIndicator color="#fff" />
-              ) : (
-                <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
-              )}
-            </TouchableOpacity>
-          </View>
-        </>
-      ) : (
-        <InitialLoadView />
-      )}
-    </KeyboardAvoidingView>
+            <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} />
+              <TouchableOpacity onPress={onPressNext}>
+                {isProcessing ? (
+                  <ActivityIndicator color="#fff" />
+                ) : (
+                  <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
+                )}
+              </TouchableOpacity>
+            </View>
+          </>
+        ) : (
+          <InitialLoadView />
+        )}
+      </KeyboardAvoidingView>
+    </ScrollView>
   )
 }
 
@@ -577,9 +617,15 @@ const styles = StyleSheet.create({
     backgroundColor: colors.blue3,
   },
   groupTitle: {
+    flexDirection: 'row',
+    alignItems: 'center',
     paddingVertical: 8,
     paddingHorizontal: 12,
   },
+  groupTitleIcon: {
+    color: colors.white,
+    marginHorizontal: 6,
+  },
   groupContent: {
     borderTopWidth: 1,
     borderTopColor: colors.blue1,
@@ -600,6 +646,22 @@ const styles = StyleSheet.create({
     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%',
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index 712d6dc23..96390e9b8 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -170,6 +170,7 @@ export const MobileShell: React.FC = observer(() => {
         <SafeAreaView style={styles.innerContainer}>
           <Login />
         </SafeAreaView>
+        <Modal />
       </LinearGradient>
     )
   }
@@ -294,6 +295,7 @@ function constructScreenRenderDesc(nav: NavigationModel): {
 const styles = StyleSheet.create({
   outerContainer: {
     height: '100%',
+    flex: 1,
   },
   innerContainer: {
     flex: 1,
diff --git a/todos.txt b/todos.txt
index e9879bf4c..da966aa24 100644
--- a/todos.txt
+++ b/todos.txt
@@ -6,6 +6,7 @@ Paul's todo list
   - Cursor behaviors on all views
   - Update swipe behaviors: edge always goes back, leftmost always goes back, main connects to selector if present
 - Onboarding flow
+  > Invite codes
   - Confirm email
   - Setup profile?
 - Onboarding
@@ -23,7 +24,7 @@ Paul's todo list
   - View on post
 - Linking
   - Web linking
-  - App linking
+  > App linking
 - Pagination
   - Liked by
   - Reposted by
@@ -33,7 +34,4 @@ Paul's todo list
 - Bugs
   - Auth token refresh seems broken
   - Check that sub components arent reloading too much
-  - Titles are getting screwed up (possibly swipe related)
-  > Dont suggest self for follows
-  > Double post on post?
-  > Handle no displayname everywhere
\ No newline at end of file
+  - Titles are getting screwed up (possibly swipe related)
\ No newline at end of file