about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json2
-rw-r--r--src/lib/icons.tsx27
-rw-r--r--src/lib/styles.ts1
-rw-r--r--src/state/index.ts2
-rw-r--r--src/state/models/ui/create-account.ts192
-rw-r--r--src/view/com/auth/LoggedOut.tsx22
-rw-r--r--src/view/com/auth/SplashScreen.tsx44
-rw-r--r--src/view/com/auth/create/Backup.tsx (renamed from src/view/com/auth/CreateAccount.tsx)8
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx241
-rw-r--r--src/view/com/auth/create/Policies.tsx101
-rw-r--r--src/view/com/auth/create/Step1.tsx187
-rw-r--r--src/view/com/auth/create/Step2.tsx275
-rw-r--r--src/view/com/auth/create/Step3.tsx44
-rw-r--r--src/view/com/auth/create/StepHeader.tsx22
-rw-r--r--src/view/com/auth/login/Login.tsx (renamed from src/view/com/auth/Signin.tsx)41
-rw-r--r--src/view/com/auth/util/HelpTip.tsx32
-rw-r--r--src/view/com/auth/util/TextInput.tsx68
-rw-r--r--src/view/com/modals/DeleteAccount.tsx6
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/util/WelcomeBanner.tsx5
-rw-r--r--src/view/com/util/error/ErrorMessage.tsx2
-rw-r--r--yarn.lock7
22 files changed, 1266 insertions, 66 deletions
diff --git a/package.json b/package.json
index 41f251921..f7264d321 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
     "js-sha256": "^0.9.0",
     "lodash.chunk": "^4.2.0",
     "lodash.clonedeep": "^4.5.0",
+    "lodash.debounce": "^4.0.8",
     "lodash.isequal": "^4.5.0",
     "lodash.omit": "^4.5.0",
     "lodash.samplesize": "^4.2.0",
@@ -122,6 +123,7 @@
     "@types/jest": "^29.4.0",
     "@types/lodash.chunk": "^4.2.7",
     "@types/lodash.clonedeep": "^4.5.7",
+    "@types/lodash.debounce": "^4.0.7",
     "@types/lodash.isequal": "^4.5.6",
     "@types/lodash.omit": "^4.5.7",
     "@types/lodash.samplesize": "^4.2.7",
diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx
index e194e7a87..fd233f99c 100644
--- a/src/lib/icons.tsx
+++ b/src/lib/icons.tsx
@@ -801,3 +801,30 @@ export function SquarePlusIcon({
     </Svg>
   )
 }
+
+export function InfoCircleIcon({
+  style,
+  size,
+  strokeWidth = 1.5,
+}: {
+  style?: StyleProp<TextStyle>
+  size?: string | number
+  strokeWidth?: number
+}) {
+  return (
+    <Svg
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={strokeWidth}
+      stroke="currentColor"
+      width={size}
+      height={size}
+      style={style}>
+      <Path
+        strokeLinecap="round"
+        strokeLinejoin="round"
+        d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
+      />
+    </Svg>
+  )
+}
diff --git a/src/lib/styles.ts b/src/lib/styles.ts
index 328229f46..5d7f7f82d 100644
--- a/src/lib/styles.ts
+++ b/src/lib/styles.ts
@@ -64,6 +64,7 @@ export const s = StyleSheet.create({
   footerSpacer: {height: 100},
   contentContainer: {paddingBottom: 200},
   contentContainerExtra: {paddingBottom: 300},
+  border0: {borderWidth: 0},
   border1: {borderWidth: 1},
   borderTop1: {borderTopWidth: 1},
   borderRight1: {borderRightWidth: 1},
diff --git a/src/state/index.ts b/src/state/index.ts
index 61b85e51d..f0713efeb 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -6,7 +6,7 @@ import * as apiPolyfill from 'lib/api/api-polyfill'
 import * as storage from 'lib/storage'
 
 export const LOCAL_DEV_SERVICE =
-  Platform.OS === 'ios' ? 'http://localhost:2583' : 'http://10.0.2.2:2583'
+  Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
 export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
 export const PROD_SERVICE = 'https://bsky.social'
 export const DEFAULT_SERVICE = PROD_SERVICE
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
new file mode 100644
index 000000000..a212fe05e
--- /dev/null
+++ b/src/state/models/ui/create-account.ts
@@ -0,0 +1,192 @@
+import {makeAutoObservable} from 'mobx'
+import {RootStoreModel} from '../root-store'
+import {ServiceDescription} from '../session'
+import {DEFAULT_SERVICE} from 'state/index'
+import {ComAtprotoAccountCreate} from '@atproto/api'
+import * as EmailValidator from 'email-validator'
+import {createFullHandle} from 'lib/strings/handles'
+import {cleanError} from 'lib/strings/errors'
+
+export class CreateAccountModel {
+  step: number = 1
+  isProcessing = false
+  isFetchingServiceDescription = false
+  didServiceDescriptionFetchFail = false
+  error = ''
+
+  serviceUrl = DEFAULT_SERVICE
+  serviceDescription: ServiceDescription | undefined = undefined
+  userDomain = ''
+  inviteCode = ''
+  email = ''
+  password = ''
+  handle = ''
+  is13 = false
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(this, {}, {autoBind: true})
+  }
+
+  // form state controls
+  // =
+
+  next() {
+    this.error = ''
+    this.step++
+  }
+
+  back() {
+    this.error = ''
+    this.step--
+  }
+
+  setStep(v: number) {
+    this.step = v
+  }
+
+  async fetchServiceDescription() {
+    this.setError('')
+    this.setIsFetchingServiceDescription(true)
+    this.setDidServiceDescriptionFetchFail(false)
+    this.setServiceDescription(undefined)
+    if (!this.serviceUrl) {
+      return
+    }
+    try {
+      const desc = await this.rootStore.session.describeService(this.serviceUrl)
+      this.setServiceDescription(desc)
+      this.setUserDomain(desc.availableUserDomains[0])
+    } catch (err: any) {
+      this.rootStore.log.warn(
+        `Failed to fetch service description for ${this.serviceUrl}`,
+        err,
+      )
+      this.setError(
+        'Unable to contact your service. Please check your Internet connection.',
+      )
+      this.setDidServiceDescriptionFetchFail(true)
+    } finally {
+      this.setIsFetchingServiceDescription(false)
+    }
+  }
+
+  async submit() {
+    if (!this.email) {
+      this.setStep(2)
+      return this.setError('Please enter your email.')
+    }
+    if (!EmailValidator.validate(this.email)) {
+      this.setStep(2)
+      return this.setError('Your email appears to be invalid.')
+    }
+    if (!this.password) {
+      this.setStep(2)
+      return this.setError('Please choose your password.')
+    }
+    if (!this.handle) {
+      this.setStep(3)
+      return this.setError('Please choose your handle.')
+    }
+    this.setError('')
+    this.setIsProcessing(true)
+    try {
+      await this.rootStore.session.createAccount({
+        service: this.serviceUrl,
+        email: this.email,
+        handle: createFullHandle(this.handle, this.userDomain),
+        password: this.password,
+        inviteCode: this.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.'
+      }
+      this.rootStore.log.error('Failed to create account', e)
+      this.setIsProcessing(false)
+      this.setError(cleanError(errMsg))
+      throw e
+    }
+  }
+
+  // form state accessors
+  // =
+
+  get canBack() {
+    return this.step > 1
+  }
+
+  get canNext() {
+    if (this.step === 1) {
+      return !!this.serviceDescription
+    } else if (this.step === 2) {
+      return (
+        (!this.isInviteCodeRequired || this.inviteCode) &&
+        !!this.email &&
+        !!this.password &&
+        this.is13
+      )
+    }
+    return !!this.handle
+  }
+
+  get isServiceDescribed() {
+    return !!this.serviceDescription
+  }
+
+  get isInviteCodeRequired() {
+    return this.serviceDescription?.inviteCodeRequired
+  }
+
+  // setters
+  // =
+
+  setIsProcessing(v: boolean) {
+    this.isProcessing = v
+  }
+
+  setIsFetchingServiceDescription(v: boolean) {
+    this.isFetchingServiceDescription = v
+  }
+
+  setDidServiceDescriptionFetchFail(v: boolean) {
+    this.didServiceDescriptionFetchFail = v
+  }
+
+  setError(v: string) {
+    this.error = v
+  }
+
+  setServiceUrl(v: string) {
+    this.serviceUrl = v
+  }
+
+  setServiceDescription(v: ServiceDescription | undefined) {
+    this.serviceDescription = v
+  }
+
+  setUserDomain(v: string) {
+    this.userDomain = v
+  }
+
+  setInviteCode(v: string) {
+    this.inviteCode = v
+  }
+
+  setEmail(v: string) {
+    this.email = v
+  }
+
+  setPassword(v: string) {
+    this.password = v
+  }
+
+  setHandle(v: string) {
+    this.handle = v
+  }
+
+  setIs13(v: boolean) {
+    this.is13 = v
+  }
+}
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 47dd51d9c..5d4b9451f 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -1,8 +1,8 @@
 import React from 'react'
 import {SafeAreaView} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {Signin} from 'view/com/auth/Signin'
-import {CreateAccount} from 'view/com/auth/CreateAccount'
+import {Login} from 'view/com/auth/login/Login'
+import {CreateAccount} from 'view/com/auth/create/CreateAccount'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -12,8 +12,8 @@ import {SplashScreen} from './SplashScreen'
 import {CenteredView} from '../util/Views'
 
 enum ScreenState {
-  S_SigninOrCreateAccount,
-  S_Signin,
+  S_LoginOrCreateAccount,
+  S_Login,
   S_CreateAccount,
 }
 
@@ -22,7 +22,7 @@ export const LoggedOut = observer(() => {
   const store = useStores()
   const {screen} = useAnalytics()
   const [screenState, setScreenState] = React.useState<ScreenState>(
-    ScreenState.S_SigninOrCreateAccount,
+    ScreenState.S_LoginOrCreateAccount,
   )
 
   React.useEffect(() => {
@@ -32,11 +32,11 @@ export const LoggedOut = observer(() => {
 
   if (
     store.session.isResumingSession ||
-    screenState === ScreenState.S_SigninOrCreateAccount
+    screenState === ScreenState.S_LoginOrCreateAccount
   ) {
     return (
       <SplashScreen
-        onPressSignin={() => setScreenState(ScreenState.S_Signin)}
+        onPressSignin={() => setScreenState(ScreenState.S_Login)}
         onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
       />
     )
@@ -46,17 +46,17 @@ export const LoggedOut = observer(() => {
     <CenteredView style={[s.hContentRegion, pal.view]}>
       <SafeAreaView testID="noSessionView" style={s.hContentRegion}>
         <ErrorBoundary>
-          {screenState === ScreenState.S_Signin ? (
-            <Signin
+          {screenState === ScreenState.S_Login ? (
+            <Login
               onPressBack={() =>
-                setScreenState(ScreenState.S_SigninOrCreateAccount)
+                setScreenState(ScreenState.S_LoginOrCreateAccount)
               }
             />
           ) : undefined}
           {screenState === ScreenState.S_CreateAccount ? (
             <CreateAccount
               onPressBack={() =>
-                setScreenState(ScreenState.S_SigninOrCreateAccount)
+                setScreenState(ScreenState.S_LoginOrCreateAccount)
               }
             />
           ) : undefined}
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index 27943f64d..f98bed120 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -1,11 +1,9 @@
 import React from 'react'
 import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
-import Image, {Source as ImageSource} from 'view/com/util/images/Image'
 import {Text} from 'view/com/util/text/Text'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
-import {colors} from 'lib/styles'
+import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {CLOUD_SPLASH} from 'lib/assets'
 import {CenteredView} from '../util/Views'
 
 export const SplashScreen = ({
@@ -17,29 +15,29 @@ export const SplashScreen = ({
 }) => {
   const pal = usePalette('default')
   return (
-    <CenteredView style={styles.container}>
-      <Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
+    <CenteredView style={[styles.container, pal.view]}>
       <SafeAreaView testID="noSessionView" style={styles.container}>
         <ErrorBoundary>
           <View style={styles.hero}>
-            <View style={styles.heroText}>
-              <Text style={styles.title}>Bluesky</Text>
-            </View>
+            <Text style={[styles.title, pal.link]}>Bluesky</Text>
+            <Text style={[styles.subtitle, pal.textLight]}>
+              See what's next
+            </Text>
           </View>
           <View testID="signinOrCreateAccount" style={styles.btns}>
             <TouchableOpacity
               testID="createAccountButton"
-              style={[pal.view, styles.btn]}
+              style={[styles.btn, {backgroundColor: colors.blue3}]}
               onPress={onPressCreateAccount}>
-              <Text style={[pal.link, styles.btnLabel]}>
+              <Text style={[s.white, styles.btnLabel]}>
                 Create a new account
               </Text>
             </TouchableOpacity>
             <TouchableOpacity
               testID="signInButton"
-              style={[pal.view, styles.btn]}
+              style={[styles.btn, pal.btn]}
               onPress={onPressSignin}>
-              <Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
+              <Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
             </TouchableOpacity>
           </View>
         </ErrorBoundary>
@@ -56,37 +54,27 @@ const styles = StyleSheet.create({
     flex: 2,
     justifyContent: 'center',
   },
-  bgImg: {
-    position: 'absolute',
-    top: 0,
-    left: 0,
-    width: '100%',
-    height: '100%',
-  },
-  heroText: {
-    backgroundColor: colors.white,
-    paddingTop: 10,
-    paddingBottom: 20,
-  },
   btns: {
     paddingBottom: 40,
   },
   title: {
     textAlign: 'center',
-    color: colors.blue3,
     fontSize: 68,
     fontWeight: 'bold',
   },
+  subtitle: {
+    textAlign: 'center',
+    fontSize: 42,
+    fontWeight: 'bold',
+  },
   btn: {
-    borderRadius: 4,
+    borderRadius: 32,
     paddingVertical: 16,
     marginBottom: 20,
     marginHorizontal: 20,
-    backgroundColor: colors.blue3,
   },
   btnLabel: {
     textAlign: 'center',
     fontSize: 21,
-    color: colors.white,
   },
 })
diff --git a/src/view/com/auth/CreateAccount.tsx b/src/view/com/auth/create/Backup.tsx
index a24dc4e35..c0693605f 100644
--- a/src/view/com/auth/CreateAccount.tsx
+++ b/src/view/com/auth/create/Backup.tsx
@@ -17,10 +17,10 @@ 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 {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'
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
new file mode 100644
index 000000000..93773665d
--- /dev/null
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -0,0 +1,241 @@
+import React from 'react'
+import {
+  ActivityIndicator,
+  KeyboardAvoidingView,
+  ScrollView,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {sha256} from 'js-sha256'
+import {useAnalytics} from 'lib/analytics'
+import {Text} from '../../util/text/Text'
+import {s, colors} from 'lib/styles'
+import {useStores} from 'state/index'
+import {CreateAccountModel} from 'state/models/ui/create-account'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+
+import {Step1} from './Step1'
+import {Step2} from './Step2'
+import {Step3} from './Step3'
+
+export const CreateAccount = observer(
+  ({onPressBack}: {onPressBack: () => void}) => {
+    const {track, screen, identify} = useAnalytics()
+    const pal = usePalette('default')
+    const store = useStores()
+    const model = React.useMemo(() => new CreateAccountModel(store), [store])
+
+    React.useEffect(() => {
+      screen('CreateAccount')
+    }, [screen])
+
+    React.useEffect(() => {
+      model.fetchServiceDescription()
+    }, [model])
+
+    const onPressRetryConnect = React.useCallback(
+      () => model.fetchServiceDescription(),
+      [model],
+    )
+
+    const onPressBackInner = React.useCallback(() => {
+      if (model.canBack) {
+        console.log('?')
+        model.back()
+      } else {
+        onPressBack()
+      }
+    }, [model, onPressBack])
+
+    const onPressNext = React.useCallback(async () => {
+      if (!model.canNext) {
+        return
+      }
+      if (model.step < 3) {
+        model.next()
+      } else {
+        try {
+          await model.submit()
+          const email_hashed = sha256(model.email)
+          identify(email_hashed, {email_hashed})
+          track('Create Account')
+        } catch {
+          // dont need to handle here
+        }
+      }
+    }, [model, identify, track])
+
+    return (
+      <ScrollView testID="createAccount" style={pal.view}>
+        <KeyboardAvoidingView behavior="padding">
+          <View style={styles.stepContainer}>
+            {model.step === 1 && <Step1 model={model} />}
+            {model.step === 2 && <Step2 model={model} />}
+            {model.step === 3 && <Step3 model={model} />}
+          </View>
+          <View style={[s.flexRow, s.pl20, s.pr20]}>
+            <TouchableOpacity onPress={onPressBackInner}>
+              <Text type="xl" style={pal.link}>
+                Back
+              </Text>
+            </TouchableOpacity>
+            <View style={s.flex1} />
+            {model.canNext ? (
+              <TouchableOpacity
+                testID="createAccountButton"
+                onPress={onPressNext}>
+                {model.isProcessing ? (
+                  <ActivityIndicator />
+                ) : (
+                  <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                    Next
+                  </Text>
+                )}
+              </TouchableOpacity>
+            ) : model.didServiceDescriptionFetchFail ? (
+              <TouchableOpacity
+                testID="registerRetryButton"
+                onPress={onPressRetryConnect}>
+                <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                  Retry
+                </Text>
+              </TouchableOpacity>
+            ) : model.isFetchingServiceDescription ? (
+              <>
+                <ActivityIndicator color="#fff" />
+                <Text type="xl" style={[pal.text, s.pr5]}>
+                  Connecting...
+                </Text>
+              </>
+            ) : undefined}
+          </View>
+          <View style={s.footerSpacer} />
+        </KeyboardAvoidingView>
+      </ScrollView>
+    )
+  },
+)
+
+const styles = StyleSheet.create({
+  stepContainer: {
+    paddingHorizontal: 20,
+    paddingVertical: 20,
+  },
+
+  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,
+  },
+})
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
new file mode 100644
index 000000000..4ba6a5406
--- /dev/null
+++ b/src/view/com/auth/create/Policies.tsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {TextLink} from '../../util/Link'
+import {Text} from '../../util/text/Text'
+import {s, colors} from 'lib/styles'
+import {ServiceDescription} from 'state/models/session'
+import {usePalette} from 'lib/hooks/usePalette'
+
+export const Policies = ({
+  serviceDescription,
+}: {
+  serviceDescription: ServiceDescription
+}) => {
+  const pal = usePalette('default')
+  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, {borderColor: pal.colors.text}, s.mt2]}>
+          <FontAwesomeIcon
+            icon="exclamation"
+            style={pal.textLight as FontAwesomeIconStyle}
+            size={10}
+          />
+        </View>
+        <Text style={[pal.textLight, 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
+        key="tos"
+        href={tos}
+        text="Terms of Service"
+        style={[pal.link, s.underline]}
+      />,
+    )
+  }
+  if (pp) {
+    els.push(
+      <TextLink
+        key="pp"
+        href={pp}
+        text="Privacy Policy"
+        style={[pal.link, s.underline]}
+      />,
+    )
+  }
+  if (els.length === 2) {
+    els.splice(
+      1,
+      0,
+      <Text key="and" style={pal.textLight}>
+        {' '}
+        and{' '}
+      </Text>,
+    )
+  }
+  return (
+    <View style={styles.policies}>
+      <Text style={pal.textLight}>
+        By creating an account you agree to the {els}.
+      </Text>
+    </View>
+  )
+}
+
+function validWebLink(url?: string): string | undefined {
+  return url && (url.startsWith('http://') || url.startsWith('https://'))
+    ? url
+    : undefined
+}
+
+const styles = StyleSheet.create({
+  policies: {
+    flexDirection: 'row',
+    alignItems: 'flex-start',
+  },
+  errorIcon: {
+    borderWidth: 1,
+    borderColor: colors.white,
+    borderRadius: 30,
+    width: 16,
+    height: 16,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginRight: 5,
+  },
+})
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
new file mode 100644
index 000000000..0a628f9d0
--- /dev/null
+++ b/src/view/com/auth/create/Step1.tsx
@@ -0,0 +1,187 @@
+import React from 'react'
+import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import debounce from 'lodash.debounce'
+import {Text} from 'view/com/util/text/Text'
+import {StepHeader} from './StepHeader'
+import {CreateAccountModel} from 'state/models/ui/create-account'
+import {useTheme} from 'lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+import {HelpTip} from '../util/HelpTip'
+import {TextInput} from '../util/TextInput'
+import {Button} from 'view/com/util/forms/Button'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+
+import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
+import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
+
+export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
+  const pal = usePalette('default')
+  const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
+
+  const onPressDefault = React.useCallback(() => {
+    setIsDefaultSelected(true)
+    model.setServiceUrl(PROD_SERVICE)
+    model.fetchServiceDescription()
+  }, [setIsDefaultSelected, model])
+
+  const onPressOther = React.useCallback(() => {
+    setIsDefaultSelected(false)
+    model.setServiceUrl('https://')
+    model.setServiceDescription(undefined)
+  }, [setIsDefaultSelected, model])
+
+  const fetchServiceDesription = React.useMemo(
+    () => debounce(() => model.fetchServiceDescription(), 1e3),
+    [model],
+  )
+
+  const onChangeServiceUrl = React.useCallback(
+    (v: string) => {
+      model.setServiceUrl(v)
+      fetchServiceDesription()
+    },
+    [model, fetchServiceDesription],
+  )
+
+  const onDebugChangeServiceUrl = React.useCallback(
+    (v: string) => {
+      model.setServiceUrl(v)
+      model.fetchServiceDescription()
+    },
+    [model],
+  )
+
+  return (
+    <View>
+      <StepHeader step="1" title="Your hosting provider" />
+      <Text style={[pal.text, s.mb10]}>
+        This is the company that keeps you online.
+      </Text>
+      <Option
+        isSelected={isDefaultSelected}
+        label="Bluesky"
+        help="&nbsp;(default)"
+        onPress={onPressDefault}
+      />
+      <Option
+        isSelected={!isDefaultSelected}
+        label="Other"
+        onPress={onPressOther}>
+        <View style={styles.otherForm}>
+          <Text style={[pal.text, s.mb5]}>
+            Enter the address of your provider:
+          </Text>
+          <TextInput
+            icon="globe"
+            placeholder="Hosting provider address"
+            value={model.serviceUrl}
+            editable
+            onChange={onChangeServiceUrl}
+          />
+          {LOGIN_INCLUDE_DEV_SERVERS && (
+            <View style={[s.flexRow, s.mt10]}>
+              <Button
+                type="default"
+                style={s.mr5}
+                label="Staging"
+                onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
+              />
+              <Button
+                type="default"
+                label="Dev Server"
+                onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
+              />
+            </View>
+          )}
+        </View>
+      </Option>
+      {model.error ? (
+        <ErrorMessage message={model.error} style={styles.error} />
+      ) : (
+        <HelpTip text="You can change hosting providers at any time." />
+      )}
+    </View>
+  )
+})
+
+function Option({
+  children,
+  isSelected,
+  label,
+  help,
+  onPress,
+}: React.PropsWithChildren<{
+  isSelected: boolean
+  label: string
+  help?: string
+  onPress: () => void
+}>) {
+  const theme = useTheme()
+  const pal = usePalette('default')
+  const circleFillStyle = React.useMemo(
+    () => ({
+      backgroundColor: theme.palette.primary.background,
+    }),
+    [theme],
+  )
+
+  return (
+    <View style={[styles.option, pal.border]}>
+      <TouchableWithoutFeedback onPress={onPress}>
+        <View style={styles.optionHeading}>
+          <View style={[styles.circle, pal.border]}>
+            {isSelected ? (
+              <View style={[circleFillStyle, styles.circleFill]} />
+            ) : undefined}
+          </View>
+          <Text type="xl" style={pal.text}>
+            {label}
+            {help ? (
+              <Text type="xl" style={pal.textLight}>
+                {help}
+              </Text>
+            ) : undefined}
+          </Text>
+        </View>
+      </TouchableWithoutFeedback>
+      {isSelected && children}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  error: {
+    borderRadius: 6,
+  },
+
+  option: {
+    borderWidth: 1,
+    borderRadius: 6,
+    marginBottom: 10,
+  },
+  optionHeading: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    padding: 10,
+  },
+  circle: {
+    width: 26,
+    height: 26,
+    borderRadius: 15,
+    padding: 4,
+    borderWidth: 1,
+    marginRight: 10,
+  },
+  circleFill: {
+    width: 16,
+    height: 16,
+    borderRadius: 10,
+  },
+
+  otherForm: {
+    paddingBottom: 10,
+    paddingHorizontal: 12,
+  },
+})
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
new file mode 100644
index 000000000..1f3162880
--- /dev/null
+++ b/src/view/com/auth/create/Step2.tsx
@@ -0,0 +1,275 @@
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {CreateAccountModel} from 'state/models/ui/create-account'
+import {Text} from 'view/com/util/text/Text'
+import {TextLink} from 'view/com/util/Link'
+import {StepHeader} from './StepHeader'
+import {s} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {TextInput} from '../util/TextInput'
+import {Policies} from './Policies'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+
+export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
+  const pal = usePalette('default')
+  return (
+    <View>
+      <StepHeader step="2" title="Your account" />
+
+      {model.isInviteCodeRequired && (
+        <View style={s.pb20}>
+          <Text type="md-medium" style={[pal.text, s.mb2]}>
+            Invite code
+          </Text>
+          <TextInput
+            icon="ticket"
+            placeholder="Required for this provider"
+            value={model.inviteCode}
+            editable
+            onChange={model.setInviteCode}
+          />
+        </View>
+      )}
+
+      {!model.inviteCode && model.isInviteCodeRequired ? (
+        <Text>
+          Don't have an invite code?{' '}
+          <TextLink text="Join the waitlist" href="#" style={pal.link} /> to try
+          the beta before it's publicly available.
+        </Text>
+      ) : (
+        <>
+          <View style={s.pb20}>
+            <Text type="md-medium" style={[pal.text, s.mb2]}>
+              Email address
+            </Text>
+            <TextInput
+              icon="envelope"
+              placeholder="Enter your email address"
+              value={model.email}
+              editable
+              onChange={model.setEmail}
+            />
+          </View>
+
+          <View style={s.pb20}>
+            <Text type="md-medium" style={[pal.text, s.mb2]}>
+              Password
+            </Text>
+            <TextInput
+              icon="lock"
+              placeholder="Choose your password"
+              value={model.password}
+              editable
+              secureTextEntry
+              onChange={model.setPassword}
+            />
+          </View>
+
+          <View style={s.pb20}>
+            <Text type="md-medium" style={[pal.text, s.mb2]}>
+              Legal check
+            </Text>
+            <TouchableOpacity
+              testID="registerIs13Input"
+              style={[styles.toggleBtn, pal.border]}
+              onPress={() => model.setIs13(!model.is13)}>
+              <View style={[pal.borderDark, styles.checkbox]}>
+                {model.is13 && (
+                  <FontAwesomeIcon icon="check" style={s.blue3} size={16} />
+                )}
+              </View>
+              <Text type="md" style={[pal.text, styles.toggleBtnLabel]}>
+                I am 13 years old or older
+              </Text>
+            </TouchableOpacity>
+          </View>
+
+          {model.serviceDescription && (
+            <Policies serviceDescription={model.serviceDescription} />
+          )}
+        </>
+      )}
+      {model.error ? (
+        <ErrorMessage message={model.error} style={styles.error} />
+      ) : undefined}
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  error: {
+    borderRadius: 6,
+    marginTop: 10,
+  },
+
+  toggleBtn: {
+    flexDirection: 'row',
+    flex: 1,
+    alignItems: 'center',
+    borderWidth: 1,
+    paddingHorizontal: 10,
+    paddingVertical: 10,
+    borderRadius: 6,
+  },
+  toggleBtnLabel: {
+    flex: 1,
+    paddingHorizontal: 10,
+  },
+
+  checkbox: {
+    borderWidth: 1,
+    borderRadius: 2,
+    width: 24,
+    height: 24,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+})
+
+/*
+
+<View style={[pal.borderDark, styles.group]}>
+{serviceDescription?.inviteCodeRequired ? (
+  <View
+    style={[pal.border, styles.groupContent, styles.noTopBorder]}>
+    <FontAwesomeIcon
+      icon="ticket"
+      style={[pal.textLight, styles.groupContentIcon]}
+    />
+    <TextInput
+      style={[pal.text, styles.textInput]}
+      placeholder="Invite code"
+      placeholderTextColor={pal.colors.textLight}
+      autoCapitalize="none"
+      autoCorrect={false}
+      autoFocus
+      keyboardAppearance={theme.colorScheme}
+      value={inviteCode}
+      onChangeText={setInviteCode}
+      onBlur={onBlurInviteCode}
+      editable={!isProcessing}
+    />
+  </View>
+) : undefined}
+<View style={[pal.border, styles.groupContent]}>
+  <FontAwesomeIcon
+    icon="envelope"
+    style={[pal.textLight, styles.groupContentIcon]}
+  />
+  <TextInput
+    testID="registerEmailInput"
+    style={[pal.text, styles.textInput]}
+    placeholder="Email address"
+    placeholderTextColor={pal.colors.textLight}
+    autoCapitalize="none"
+    autoCorrect={false}
+    value={email}
+    onChangeText={setEmail}
+    editable={!isProcessing}
+  />
+</View>
+<View style={[pal.border, styles.groupContent]}>
+  <FontAwesomeIcon
+    icon="lock"
+    style={[pal.textLight, styles.groupContentIcon]}
+  />
+  <TextInput
+    testID="registerPasswordInput"
+    style={[pal.text, styles.textInput]}
+    placeholder="Choose your password"
+    placeholderTextColor={pal.colors.textLight}
+    autoCapitalize="none"
+    autoCorrect={false}
+    secureTextEntry
+    value={password}
+    onChangeText={setPassword}
+    editable={!isProcessing}
+  />
+</View>
+</View>
+</>
+) : undefined}
+{serviceDescription ? (
+<>
+<View style={styles.groupLabel}>
+<Text type="sm-bold" style={pal.text}>
+  Choose your username
+</Text>
+</View>
+<View style={[pal.border, styles.group]}>
+<View
+  style={[pal.border, styles.groupContent, styles.noTopBorder]}>
+  <FontAwesomeIcon
+    icon="at"
+    style={[pal.textLight, styles.groupContentIcon]}
+  />
+  <TextInput
+    testID="registerHandleInput"
+    style={[pal.text, styles.textInput]}
+    placeholder="eg alice"
+    placeholderTextColor={pal.colors.textLight}
+    autoCapitalize="none"
+    value={handle}
+    onChangeText={v => setHandle(makeValidHandle(v))}
+    editable={!isProcessing}
+  />
+</View>
+{serviceDescription.availableUserDomains.length > 1 && (
+  <View style={[pal.border, styles.groupContent]}>
+    <FontAwesomeIcon
+      icon="globe"
+      style={styles.groupContentIcon}
+    />
+    <Picker
+      style={[pal.text, styles.picker]}
+      labelStyle={styles.pickerLabel}
+      iconStyle={pal.textLight as FontAwesomeIconStyle}
+      value={userDomain}
+      items={serviceDescription.availableUserDomains.map(d => ({
+        label: `.${d}`,
+        value: d,
+      }))}
+      onChange={itemValue => setUserDomain(itemValue)}
+      enabled={!isProcessing}
+    />
+  </View>
+)}
+<View style={[pal.border, styles.groupContent]}>
+  <Text style={[pal.textLight, s.p10]}>
+    Your full username will be{' '}
+    <Text type="md-bold" style={pal.textLight}>
+      @{createFullHandle(handle, userDomain)}
+    </Text>
+  </Text>
+</View>
+</View>
+<View style={styles.groupLabel}>
+<Text type="sm-bold" style={pal.text}>
+  Legal
+</Text>
+</View>
+<View style={[pal.border, styles.group]}>
+<View
+  style={[pal.border, styles.groupContent, styles.noTopBorder]}>
+  <TouchableOpacity
+    testID="registerIs13Input"
+    style={styles.textBtn}
+    onPress={() => setIs13(!is13)}>
+    <View
+      style={[
+        pal.border,
+        is13 ? styles.checkboxFilled : styles.checkbox,
+      ]}>
+      {is13 && (
+        <FontAwesomeIcon icon="check" style={s.blue3} size={14} />
+      )}
+    </View>
+    <Text style={[pal.text, styles.textBtnLabel]}>
+      I am 13 years old or older
+    </Text>
+  </TouchableOpacity>
+</View>
+</View>*/
diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx
new file mode 100644
index 000000000..652591171
--- /dev/null
+++ b/src/view/com/auth/create/Step3.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {CreateAccountModel} from 'state/models/ui/create-account'
+import {Text} from 'view/com/util/text/Text'
+import {StepHeader} from './StepHeader'
+import {s} from 'lib/styles'
+import {TextInput} from '../util/TextInput'
+import {createFullHandle} from 'lib/strings/handles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+
+export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
+  const pal = usePalette('default')
+  return (
+    <View>
+      <StepHeader step="3" title="Your user handle" />
+      <View style={s.pb10}>
+        <TextInput
+          icon="at"
+          placeholder="eg alice"
+          value={model.handle}
+          editable
+          onChange={model.setHandle}
+        />
+        <Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
+          Your full handle will be{' '}
+          <Text type="lg-bold" style={pal.text}>
+            @{createFullHandle(model.handle, model.userDomain)}
+          </Text>
+        </Text>
+      </View>
+      {model.error ? (
+        <ErrorMessage message={model.error} style={styles.error} />
+      ) : undefined}
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  error: {
+    borderRadius: 6,
+  },
+})
diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx
new file mode 100644
index 000000000..8c852b640
--- /dev/null
+++ b/src/view/com/auth/create/StepHeader.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {Text} from 'view/com/util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+
+export function StepHeader({step, title}: {step: string; title: string}) {
+  const pal = usePalette('default')
+  return (
+    <View style={styles.container}>
+      <Text type="lg" style={pal.textLight}>
+        {step === '3' ? 'Last step!' : <>Step {step} of 3</>}
+      </Text>
+      <Text type="title-xl">{title}</Text>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    marginBottom: 20,
+  },
+})
diff --git a/src/view/com/auth/Signin.tsx b/src/view/com/auth/login/Login.tsx
index 6faf5ff12..f99e72daa 100644
--- a/src/view/com/auth/Signin.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -15,9 +15,8 @@ import {
 import * as EmailValidator from 'email-validator'
 import AtpAgent from '@atproto/api'
 import {useAnalytics} from 'lib/analytics'
-import {LogoTextHero} from './Logo'
-import {Text} from '../util/text/Text'
-import {UserAvatar} from '../util/UserAvatar'
+import {Text} from '../../util/text/Text'
+import {UserAvatar} from '../../util/UserAvatar'
 import {s, colors} from 'lib/styles'
 import {createFullHandle} from 'lib/strings/handles'
 import {toNiceDomain} from 'lib/strings/url-helpers'
@@ -37,7 +36,7 @@ enum Forms {
   PasswordUpdated,
 }
 
-export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
+export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   const pal = usePalette('default')
   const store = useStores()
   const {track} = useAnalytics()
@@ -100,7 +99,10 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
   }
 
   return (
-    <KeyboardAvoidingView testID="signIn" behavior="padding" style={[pal.view]}>
+    <KeyboardAvoidingView
+      testID="signIn"
+      behavior="padding"
+      style={[pal.view, s.pt10]}>
       {currentForm === Forms.Login ? (
         <LoginForm
           store={store}
@@ -164,9 +166,9 @@ const ChooseAccountForm = ({
   const pal = usePalette('default')
   const [isProcessing, setIsProcessing] = React.useState(false)
 
-  // React.useEffect(() => {
-  screen('Choose Account')
-  // }, [screen])
+  React.useEffect(() => {
+    screen('Choose Account')
+  }, [screen])
 
   const onTryAccount = async (account: AccountData) => {
     if (account.accessJwt && account.refreshJwt) {
@@ -183,15 +185,16 @@ const ChooseAccountForm = ({
 
   return (
     <View testID="chooseAccountForm">
-      <LogoTextHero />
-      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
+      <Text
+        type="2xl-medium"
+        style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
         Sign in as...
       </Text>
       {store.session.accounts.map(account => (
         <TouchableOpacity
           testID={`chooseAccountBtn-${account.handle}`}
           key={account.did}
-          style={[pal.borderDark, styles.group, s.mb5]}
+          style={[pal.view, pal.border, styles.account]}
           onPress={() => onTryAccount(account)}>
           <View
             style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
@@ -216,7 +219,7 @@ const ChooseAccountForm = ({
       ))}
       <TouchableOpacity
         testID="chooseNewAccountBtn"
-        style={[pal.borderDark, styles.group]}
+        style={[pal.view, pal.border, styles.account, styles.accountLast]}
         onPress={() => onSelectAccount(undefined)}>
         <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
           <Text style={[styles.accountText, styles.accountTextOther]}>
@@ -336,7 +339,6 @@ const LoginForm = ({
   const isReady = !!serviceDescription && !!identifier && !!password
   return (
     <View testID="loginForm">
-      <LogoTextHero />
       <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
         Sign into
       </Text>
@@ -523,7 +525,6 @@ const ForgotPasswordForm = ({
 
   return (
     <>
-      <LogoTextHero />
       <View>
         <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
           Reset password
@@ -669,7 +670,6 @@ const SetNewPasswordForm = ({
 
   return (
     <>
-      <LogoTextHero />
       <View>
         <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
           Set new password
@@ -774,7 +774,6 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
   const pal = usePalette('default')
   return (
     <>
-      <LogoTextHero />
       <View>
         <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
           Password updated!
@@ -825,6 +824,16 @@ const styles = StyleSheet.create({
   groupContentIcon: {
     marginLeft: 10,
   },
+  account: {
+    borderTopWidth: 1,
+    paddingHorizontal: 20,
+    paddingVertical: 4,
+  },
+  accountLast: {
+    borderBottomWidth: 1,
+    marginBottom: 20,
+    paddingVertical: 8,
+  },
   textInput: {
     flex: 1,
     width: '100%',
diff --git a/src/view/com/auth/util/HelpTip.tsx b/src/view/com/auth/util/HelpTip.tsx
new file mode 100644
index 000000000..3ea4437df
--- /dev/null
+++ b/src/view/com/auth/util/HelpTip.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {Text} from 'view/com/util/text/Text'
+import {InfoCircleIcon} from 'lib/icons'
+import {s, colors} from 'lib/styles'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+
+export function HelpTip({text}: {text: string}) {
+  const bg = useColorSchemeStyle(
+    {backgroundColor: colors.gray1},
+    {backgroundColor: colors.gray8},
+  )
+  const fg = useColorSchemeStyle({color: colors.gray5}, {color: colors.gray4})
+  return (
+    <View style={[styles.helptip, bg]}>
+      <InfoCircleIcon size={18} style={fg} strokeWidth={1.5} />
+      <Text type="xs-medium" style={[fg, s.ml5]}>
+        {text}
+      </Text>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  helptip: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 6,
+    paddingHorizontal: 10,
+    paddingVertical: 8,
+  },
+})
diff --git a/src/view/com/auth/util/TextInput.tsx b/src/view/com/auth/util/TextInput.tsx
new file mode 100644
index 000000000..934bf2acf
--- /dev/null
+++ b/src/view/com/auth/util/TextInput.tsx
@@ -0,0 +1,68 @@
+import React from 'react'
+import {StyleSheet, TextInput as RNTextInput, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+
+export function TextInput({
+  testID,
+  icon,
+  value,
+  placeholder,
+  editable,
+  secureTextEntry,
+  onChange,
+}: {
+  testID?: string
+  icon: IconProp
+  value: string
+  placeholder: string
+  editable: boolean
+  secureTextEntry?: boolean
+  onChange: (v: string) => void
+}) {
+  const theme = useTheme()
+  const pal = usePalette('default')
+  return (
+    <View style={[pal.border, styles.container]}>
+      <FontAwesomeIcon icon={icon} style={[pal.textLight, styles.icon]} />
+      <RNTextInput
+        testID={testID}
+        style={[pal.text, styles.textInput]}
+        placeholder={placeholder}
+        placeholderTextColor={pal.colors.textLight}
+        autoCapitalize="none"
+        autoCorrect={false}
+        keyboardAppearance={theme.colorScheme}
+        secureTextEntry={secureTextEntry}
+        value={value}
+        onChangeText={v => onChange(v)}
+        editable={editable}
+      />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    borderWidth: 1,
+    borderRadius: 6,
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 4,
+  },
+  icon: {
+    marginLeft: 10,
+  },
+  textInput: {
+    flex: 1,
+    width: '100%',
+    paddingVertical: 10,
+    paddingHorizontal: 10,
+    fontSize: 17,
+    letterSpacing: 0.25,
+    fontWeight: '400',
+    borderRadius: 10,
+  },
+})
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 62fa9f386..23cd9eb82 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -5,7 +5,7 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {BottomSheetTextInput} from '@gorhom/bottom-sheet'
+import {TextInput} from './util'
 import LinearGradient from 'react-native-linear-gradient'
 import * as Toast from '../util/Toast'
 import {Text} from '../util/text/Text'
@@ -116,7 +116,7 @@ export function Component({}: {}) {
               Check your inbox for an email with the confirmation code to enter
               below:
             </Text>
-            <BottomSheetTextInput
+            <TextInput
               style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
               placeholder="Confirmation code"
               placeholderTextColor={pal.textLight.color}
@@ -127,7 +127,7 @@ export function Component({}: {}) {
             <Text type="lg" style={styles.description}>
               Please enter your password as well:
             </Text>
-            <BottomSheetTextInput
+            <TextInput
               style={[styles.textInput, pal.borderDark, pal.text]}
               placeholder="Password"
               placeholderTextColor={pal.textLight.color}
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index dd9a3aa65..0627fa9b6 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -10,6 +10,7 @@ import * as EditProfileModal from './EditProfile'
 import * as ServerInputModal from './ServerInput'
 import * as ReportPostModal from './ReportPost'
 import * as ReportAccountModal from './ReportAccount'
+import * as DeleteAccountModal from './DeleteAccount'
 import * as RepostModal from './Repost'
 import * as CropImageModal from './crop-image/CropImage.web'
 import * as ChangeHandleModal from './ChangeHandle'
@@ -61,6 +62,8 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <ReportAccountModal.Component {...modal} />
   } else if (modal.name === 'crop-image') {
     element = <CropImageModal.Component {...modal} />
+  } else if (modal.name === 'delete-account') {
+    element = <DeleteAccountModal.Component />
   } else if (modal.name === 'repost') {
     element = <RepostModal.Component {...modal} />
   } else if (modal.name === 'change-handle') {
diff --git a/src/view/com/util/WelcomeBanner.tsx b/src/view/com/util/WelcomeBanner.tsx
index 9e360f725..428a30764 100644
--- a/src/view/com/util/WelcomeBanner.tsx
+++ b/src/view/com/util/WelcomeBanner.tsx
@@ -10,6 +10,7 @@ import {useStores} from 'state/index'
 import {SUGGESTED_FOLLOWS} from 'lib/constants'
 // @ts-ignore no type definition -prf
 import ProgressBar from 'react-native-progress/Bar'
+import {CenteredView} from './Views'
 
 export const WelcomeBanner = observer(() => {
   const pal = usePalette('default')
@@ -39,7 +40,7 @@ export const WelcomeBanner = observer(() => {
   }, [store])
 
   return (
-    <View
+    <CenteredView
       testID="welcomeBanner"
       style={[pal.view, styles.container, pal.border]}>
       <Text
@@ -76,7 +77,7 @@ export const WelcomeBanner = observer(() => {
           </View>
         </>
       )}
-    </View>
+    </CenteredView>
   )
 })
 
diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx
index e6e27fac0..cc0df1b59 100644
--- a/src/view/com/util/error/ErrorMessage.tsx
+++ b/src/view/com/util/error/ErrorMessage.tsx
@@ -38,7 +38,7 @@ export function ErrorMessage({
         />
       </View>
       <Text
-        type="sm"
+        type="sm-medium"
         style={[styles.message, pal.text]}
         numberOfLines={numberOfLines}>
         {message}
diff --git a/yarn.lock b/yarn.lock
index c9032b71a..43600fa97 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3508,6 +3508,13 @@
   dependencies:
     "@types/lodash" "*"
 
+"@types/lodash.debounce@^4.0.7":
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
+  integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==
+  dependencies:
+    "@types/lodash" "*"
+
 "@types/lodash.isequal@^4.5.6":
   version "4.5.6"
   resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz#ff42a1b8e20caa59a97e446a77dc57db923bc02b"