about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-03-20 23:29:24 +0000
committerGitHub <noreply@github.com>2024-03-20 23:29:24 +0000
commitc649ee1afa80f71f108187df5671ae85eeaeed99 (patch)
treeadb5227f58811d0fe4af023184f9ffd71f66f463
parent8ad813cd86c74a987cb81f5278c2eabbe8193db8 (diff)
parentd2d4d3a09206b52fe78018b89f82471c3dd91c8a (diff)
downloadvoidsky-c649ee1afa80f71f108187df5671ae85eeaeed99.tar.zst
Merge pull request #3217 from bluesky-social/samuel/alf-login
Use ALF for login & signup flow
-rw-r--r--assets/icons/calendar_stroke2_corner0_rounded.svg1
-rw-r--r--assets/icons/envelope_stroke2_corner0_rounded.svg1
-rw-r--r--assets/icons/lock_stroke2_corner0_rounded.svg1
-rw-r--r--assets/icons/pencilLine_stroke2_corner0_rounded.svg1
-rw-r--r--assets/icons/ticket_stroke2_corner0_rounded.svg1
-rw-r--r--package.json2
-rw-r--r--src/alf/atoms.ts84
-rw-r--r--src/alf/tokens.ts8
-rw-r--r--src/components/forms/DateField/index.android.tsx83
-rw-r--r--src/components/forms/DateField/index.shared.tsx99
-rw-r--r--src/components/forms/DateField/index.tsx66
-rw-r--r--src/components/forms/DateField/index.web.tsx10
-rw-r--r--src/components/forms/DateField/types.ts1
-rw-r--r--src/components/forms/FormError.tsx30
-rw-r--r--src/components/forms/HostingProvider.tsx95
-rw-r--r--src/components/forms/TextField.tsx14
-rw-r--r--src/components/icons/Calendar.tsx5
-rw-r--r--src/components/icons/Envelope.tsx5
-rw-r--r--src/components/icons/Lock.tsx5
-rw-r--r--src/components/icons/Pencil.tsx5
-rw-r--r--src/components/icons/Ticket.tsx5
-rw-r--r--src/lib/strings/handles.ts2
-rw-r--r--src/screens/Login/ChooseAccountForm.tsx188
-rw-r--r--src/screens/Login/ForgotPasswordForm.tsx184
-rw-r--r--src/screens/Login/FormContainer.tsx53
-rw-r--r--src/screens/Login/LoginForm.tsx266
-rw-r--r--src/screens/Login/PasswordUpdatedForm.tsx50
-rw-r--r--src/screens/Login/ScreenTransition.tsx10
-rw-r--r--src/screens/Login/ScreenTransition.web.tsx1
-rw-r--r--src/screens/Login/SetNewPasswordForm.tsx192
-rw-r--r--src/screens/Login/index.tsx174
-rw-r--r--src/screens/Signup/StepCaptcha/CaptchaWebView.tsx (renamed from src/view/com/auth/create/CaptchaWebView.tsx)17
-rw-r--r--src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx (renamed from src/view/com/auth/create/CaptchaWebView.web.tsx)0
-rw-r--r--src/screens/Signup/StepCaptcha/index.tsx95
-rw-r--r--src/screens/Signup/StepHandle.tsx134
-rw-r--r--src/screens/Signup/StepInfo/Policies.tsx97
-rw-r--r--src/screens/Signup/StepInfo/index.tsx146
-rw-r--r--src/screens/Signup/index.tsx211
-rw-r--r--src/screens/Signup/state.ts320
-rw-r--r--src/view/com/auth/LoggedOut.tsx18
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx230
-rw-r--r--src/view/com/auth/create/Policies.tsx121
-rw-r--r--src/view/com/auth/create/Step1.tsx261
-rw-r--r--src/view/com/auth/create/Step2.tsx140
-rw-r--r--src/view/com/auth/create/Step3.tsx114
-rw-r--r--src/view/com/auth/create/StepHeader.tsx44
-rw-r--r--src/view/com/auth/create/state.ts298
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx167
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx228
-rw-r--r--src/view/com/auth/login/Login.tsx164
-rw-r--r--src/view/com/auth/login/LoginForm.tsx301
-rw-r--r--src/view/com/auth/login/PasswordUpdatedForm.tsx48
-rw-r--r--src/view/com/auth/login/SetNewPasswordForm.tsx211
-rw-r--r--src/view/com/auth/login/styles.ts118
54 files changed, 2570 insertions, 2555 deletions
diff --git a/assets/icons/calendar_stroke2_corner0_rounded.svg b/assets/icons/calendar_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..703f389db
--- /dev/null
+++ b/assets/icons/calendar_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 2a1 1 0 0 1 1 1v1h6V3a1 1 0 1 1 2 0v1h2a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1ZM5 6v3h14V6H5Zm14 5H5v8h14v-8Z" clip-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/assets/icons/envelope_stroke2_corner0_rounded.svg b/assets/icons/envelope_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..c3ab45980
--- /dev/null
+++ b/assets/icons/envelope_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z" clip-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/assets/icons/lock_stroke2_corner0_rounded.svg b/assets/icons/lock_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..8b094ba5e
--- /dev/null
+++ b/assets/icons/lock_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/assets/icons/pencilLine_stroke2_corner0_rounded.svg b/assets/icons/pencilLine_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..c58bef9fa
--- /dev/null
+++ b/assets/icons/pencilLine_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>
diff --git a/assets/icons/ticket_stroke2_corner0_rounded.svg b/assets/icons/ticket_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..a45a90ae5
--- /dev/null
+++ b/assets/icons/ticket_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" stroke="#000" stroke-linejoin="round" d="M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z"/></svg>
diff --git a/package.json b/package.json
index 0858dc38a..eaf242a5f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "bsky.app",
-  "version": "1.73.0",
+  "version": "1.74.0",
   "private": true,
   "engines": {
     "node": ">=18"
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 0b473ba90..45ab72ca6 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -1,6 +1,7 @@
 import {Platform} from 'react-native'
-import {web, native} from '#/alf/util/platform'
+
 import * as tokens from '#/alf/tokens'
+import {native, web} from '#/alf/util/platform'
 
 export const atoms = {
   /*
@@ -157,6 +158,12 @@ export const atoms = {
   align_end: {
     alignItems: 'flex-end',
   },
+  align_baseline: {
+    alignItems: 'baseline',
+  },
+  align_stretch: {
+    alignItems: 'stretch',
+  },
   self_auto: {
     alignSelf: 'auto',
   },
@@ -247,10 +254,10 @@ export const atoms = {
     fontWeight: tokens.fontWeight.normal,
   },
   font_semibold: {
-    fontWeight: '500',
+    fontWeight: tokens.fontWeight.semibold,
   },
   font_bold: {
-    fontWeight: tokens.fontWeight.semibold,
+    fontWeight: tokens.fontWeight.bold,
   },
   italic: {
     fontStyle: 'italic',
@@ -300,6 +307,9 @@ export const atoms = {
   /*
    * Padding
    */
+  p_0: {
+    padding: 0,
+  },
   p_2xs: {
     padding: tokens.space._2xs,
   },
@@ -330,6 +340,10 @@ export const atoms = {
   p_5xl: {
     padding: tokens.space._5xl,
   },
+  px_0: {
+    paddingLeft: 0,
+    paddingRight: 0,
+  },
   px_2xs: {
     paddingLeft: tokens.space._2xs,
     paddingRight: tokens.space._2xs,
@@ -370,6 +384,10 @@ export const atoms = {
     paddingLeft: tokens.space._5xl,
     paddingRight: tokens.space._5xl,
   },
+  py_0: {
+    paddingTop: 0,
+    paddingBottom: 0,
+  },
   py_2xs: {
     paddingTop: tokens.space._2xs,
     paddingBottom: tokens.space._2xs,
@@ -410,6 +428,9 @@ export const atoms = {
     paddingTop: tokens.space._5xl,
     paddingBottom: tokens.space._5xl,
   },
+  pt_0: {
+    paddingTop: 0,
+  },
   pt_2xs: {
     paddingTop: tokens.space._2xs,
   },
@@ -440,6 +461,9 @@ export const atoms = {
   pt_5xl: {
     paddingTop: tokens.space._5xl,
   },
+  pb_0: {
+    paddingBottom: 0,
+  },
   pb_2xs: {
     paddingBottom: tokens.space._2xs,
   },
@@ -470,6 +494,9 @@ export const atoms = {
   pb_5xl: {
     paddingBottom: tokens.space._5xl,
   },
+  pl_0: {
+    paddingLeft: 0,
+  },
   pl_2xs: {
     paddingLeft: tokens.space._2xs,
   },
@@ -500,6 +527,9 @@ export const atoms = {
   pl_5xl: {
     paddingLeft: tokens.space._5xl,
   },
+  pr_0: {
+    paddingRight: 0,
+  },
   pr_2xs: {
     paddingRight: tokens.space._2xs,
   },
@@ -534,9 +564,8 @@ export const atoms = {
   /*
    * Margin
    */
-  mx_auto: {
-    marginLeft: 'auto',
-    marginRight: 'auto',
+  m_0: {
+    margin: 0,
   },
   m_2xs: {
     margin: tokens.space._2xs,
@@ -568,6 +597,13 @@ export const atoms = {
   m_5xl: {
     margin: tokens.space._5xl,
   },
+  m_auto: {
+    margin: 'auto',
+  },
+  mx_0: {
+    marginLeft: 0,
+    marginRight: 0,
+  },
   mx_2xs: {
     marginLeft: tokens.space._2xs,
     marginRight: tokens.space._2xs,
@@ -608,6 +644,14 @@ export const atoms = {
     marginLeft: tokens.space._5xl,
     marginRight: tokens.space._5xl,
   },
+  mx_auto: {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+  },
+  my_0: {
+    marginTop: 0,
+    marginBottom: 0,
+  },
   my_2xs: {
     marginTop: tokens.space._2xs,
     marginBottom: tokens.space._2xs,
@@ -648,6 +692,13 @@ export const atoms = {
     marginTop: tokens.space._5xl,
     marginBottom: tokens.space._5xl,
   },
+  my_auto: {
+    marginTop: 'auto',
+    marginBottom: 'auto',
+  },
+  mt_0: {
+    marginTop: 0,
+  },
   mt_2xs: {
     marginTop: tokens.space._2xs,
   },
@@ -678,6 +729,12 @@ export const atoms = {
   mt_5xl: {
     marginTop: tokens.space._5xl,
   },
+  mt_auto: {
+    marginTop: 'auto',
+  },
+  mb_0: {
+    marginBottom: 0,
+  },
   mb_2xs: {
     marginBottom: tokens.space._2xs,
   },
@@ -708,6 +765,12 @@ export const atoms = {
   mb_5xl: {
     marginBottom: tokens.space._5xl,
   },
+  mb_auto: {
+    marginBottom: 'auto',
+  },
+  ml_0: {
+    marginLeft: 0,
+  },
   ml_2xs: {
     marginLeft: tokens.space._2xs,
   },
@@ -738,6 +801,12 @@ export const atoms = {
   ml_5xl: {
     marginLeft: tokens.space._5xl,
   },
+  ml_auto: {
+    marginLeft: 'auto',
+  },
+  mr_0: {
+    marginRight: 0,
+  },
   mr_2xs: {
     marginRight: tokens.space._2xs,
   },
@@ -768,4 +837,7 @@ export const atoms = {
   mr_5xl: {
     marginRight: tokens.space._5xl,
   },
+  mr_auto: {
+    marginRight: 'auto',
+  },
 } as const
diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts
index 4045c831c..1bddd95d4 100644
--- a/src/alf/tokens.ts
+++ b/src/alf/tokens.ts
@@ -1,8 +1,8 @@
 import {
   BLUE_HUE,
-  RED_HUE,
-  GREEN_HUE,
   generateScale,
+  GREEN_HUE,
+  RED_HUE,
 } from '#/alf/util/colorGeneration'
 
 export const scale = generateScale(6, 100)
@@ -116,8 +116,8 @@ export const borderRadius = {
 
 export const fontWeight = {
   normal: '400',
-  semibold: '600',
-  bold: '900',
+  semibold: '500',
+  bold: '600',
 } as const
 
 export const gradients = {
diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx
index 451810a5e..700d15e6d 100644
--- a/src/components/forms/DateField/index.android.tsx
+++ b/src/components/forms/DateField/index.android.tsx
@@ -1,19 +1,11 @@
 import React from 'react'
-import {View, Pressable} from 'react-native'
-
-import {useTheme, atoms} from '#/alf'
-import {Text} from '#/components/Typography'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
-import * as TextField from '#/components/forms/TextField'
-import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
+import DatePicker from 'react-native-date-picker'
 
+import {useTheme} from '#/alf'
 import {DateFieldProps} from '#/components/forms/DateField/types'
-import {
-  localizeDate,
-  toSimpleDateString,
-} from '#/components/forms/DateField/utils'
-import DatePicker from 'react-native-date-picker'
-import {isAndroid} from 'platform/detection'
+import {toSimpleDateString} from '#/components/forms/DateField/utils'
+import * as TextField from '#/components/forms/TextField'
+import {DateFieldButton} from './index.shared'
 
 export * as utils from '#/components/forms/DateField/utils'
 export const Label = TextField.Label
@@ -24,18 +16,10 @@ export function DateField({
   label,
   isInvalid,
   testID,
+  accessibilityHint,
 }: DateFieldProps) {
   const t = useTheme()
   const [open, setOpen] = React.useState(false)
-  const {
-    state: pressed,
-    onIn: onPressIn,
-    onOut: onPressOut,
-  } = useInteractionState()
-  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-
-  const {chromeFocus, chromeError, chromeErrorHover} =
-    TextField.useSharedInputStyles()
 
   const onChangeInternal = React.useCallback(
     (date: Date) => {
@@ -47,50 +31,29 @@ export function DateField({
     [onChangeDate, setOpen],
   )
 
+  const onPress = React.useCallback(() => {
+    setOpen(true)
+  }, [])
+
   const onCancel = React.useCallback(() => {
     setOpen(false)
   }, [])
 
   return (
-    <View style={[atoms.relative, atoms.w_full]}>
-      <Pressable
-        aria-label={label}
-        accessibilityLabel={label}
-        accessibilityHint={undefined}
-        onPress={() => setOpen(true)}
-        onPressIn={onPressIn}
-        onPressOut={onPressOut}
-        onFocus={onFocus}
-        onBlur={onBlur}
-        style={[
-          {
-            paddingTop: 16,
-            paddingBottom: 16,
-            borderColor: 'transparent',
-            borderWidth: 2,
-          },
-          atoms.flex_row,
-          atoms.flex_1,
-          atoms.w_full,
-          atoms.px_lg,
-          atoms.rounded_sm,
-          t.atoms.bg_contrast_50,
-          focused || pressed ? chromeFocus : {},
-          isInvalid ? chromeError : {},
-          isInvalid && (focused || pressed) ? chromeErrorHover : {},
-        ]}>
-        <TextField.Icon icon={CalendarDays} />
-
-        <Text
-          style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}>
-          {localizeDate(value)}
-        </Text>
-      </Pressable>
+    <>
+      <DateFieldButton
+        label={label}
+        value={value}
+        onPress={onPress}
+        isInvalid={isInvalid}
+        accessibilityHint={accessibilityHint}
+      />
 
       {open && (
         <DatePicker
-          modal={isAndroid}
-          open={isAndroid}
+          modal
+          open
+          timeZoneOffsetInMinutes={0}
           theme={t.name === 'light' ? 'light' : 'dark'}
           date={new Date(value)}
           onConfirm={onChangeInternal}
@@ -99,9 +62,9 @@ export function DateField({
           testID={`${testID}-datepicker`}
           aria-label={label}
           accessibilityLabel={label}
-          accessibilityHint={undefined}
+          accessibilityHint={accessibilityHint}
         />
       )}
-    </View>
+    </>
   )
 }
diff --git a/src/components/forms/DateField/index.shared.tsx b/src/components/forms/DateField/index.shared.tsx
new file mode 100644
index 000000000..1f54bdc8b
--- /dev/null
+++ b/src/components/forms/DateField/index.shared.tsx
@@ -0,0 +1,99 @@
+import React from 'react'
+import {Pressable, View} from 'react-native'
+
+import {android, atoms as a, useTheme, web} from '#/alf'
+import * as TextField from '#/components/forms/TextField'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
+import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
+import {Text} from '#/components/Typography'
+import {localizeDate} from './utils'
+
+// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
+// iOS: open a dialog with an inline date picker
+// Android: open the date picker modal
+
+export function DateFieldButton({
+  label,
+  value,
+  onPress,
+  isInvalid,
+  accessibilityHint,
+}: {
+  label: string
+  value: string
+  onPress: () => void
+  isInvalid?: boolean
+  accessibilityHint?: string
+}) {
+  const t = useTheme()
+
+  const {
+    state: pressed,
+    onIn: onPressIn,
+    onOut: onPressOut,
+  } = useInteractionState()
+  const {
+    state: hovered,
+    onIn: onHoverIn,
+    onOut: onHoverOut,
+  } = useInteractionState()
+  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
+
+  const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
+    TextField.useSharedInputStyles()
+
+  return (
+    <View
+      style={[a.relative, a.w_full]}
+      {...web({
+        onMouseOver: onHoverIn,
+        onMouseOut: onHoverOut,
+      })}>
+      <Pressable
+        aria-label={label}
+        accessibilityLabel={label}
+        accessibilityHint={accessibilityHint}
+        onPress={onPress}
+        onPressIn={onPressIn}
+        onPressOut={onPressOut}
+        onFocus={onFocus}
+        onBlur={onBlur}
+        style={[
+          {
+            paddingTop: 12,
+            paddingBottom: 12,
+            paddingLeft: 14,
+            paddingRight: 14,
+            borderColor: 'transparent',
+            borderWidth: 2,
+          },
+          android({
+            minHeight: 57.5,
+          }),
+          a.flex_row,
+          a.flex_1,
+          a.w_full,
+          a.rounded_sm,
+          t.atoms.bg_contrast_25,
+          a.align_center,
+          hovered ? chromeHover : {},
+          focused || pressed ? chromeFocus : {},
+          isInvalid || isInvalid ? chromeError : {},
+          (isInvalid || isInvalid) && (hovered || focused)
+            ? chromeErrorHover
+            : {},
+        ]}>
+        <TextField.Icon icon={CalendarDays} />
+        <Text
+          style={[
+            a.text_md,
+            a.pl_xs,
+            t.atoms.text,
+            {lineHeight: a.text_md.fontSize * 1.1875},
+          ]}>
+          {localizeDate(value)}
+        </Text>
+      </Pressable>
+    </View>
+  )
+}
diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx
index 49e47a01e..5662bb594 100644
--- a/src/components/forms/DateField/index.tsx
+++ b/src/components/forms/DateField/index.tsx
@@ -1,11 +1,16 @@
 import React from 'react'
 import {View} from 'react-native'
+import DatePicker from 'react-native-date-picker'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
-import {useTheme, atoms} from '#/alf'
-import * as TextField from '#/components/forms/TextField'
-import {toSimpleDateString} from '#/components/forms/DateField/utils'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
 import {DateFieldProps} from '#/components/forms/DateField/types'
-import DatePicker from 'react-native-date-picker'
+import {toSimpleDateString} from '#/components/forms/DateField/utils'
+import * as TextField from '#/components/forms/TextField'
+import {DateFieldButton} from './index.shared'
 
 export * as utils from '#/components/forms/DateField/utils'
 export const Label = TextField.Label
@@ -22,8 +27,12 @@ export function DateField({
   onChangeDate,
   testID,
   label,
+  isInvalid,
+  accessibilityHint,
 }: DateFieldProps) {
+  const {_} = useLingui()
   const t = useTheme()
+  const control = Dialog.useDialogControl()
 
   const onChangeInternal = React.useCallback(
     (date: Date | undefined) => {
@@ -36,17 +45,44 @@ export function DateField({
   )
 
   return (
-    <View style={[atoms.relative, atoms.w_full]}>
-      <DatePicker
-        theme={t.name === 'light' ? 'light' : 'dark'}
-        date={new Date(value)}
-        onDateChange={onChangeInternal}
-        mode="date"
-        testID={`${testID}-datepicker`}
-        aria-label={label}
-        accessibilityLabel={label}
-        accessibilityHint={undefined}
+    <>
+      <DateFieldButton
+        label={label}
+        value={value}
+        onPress={control.open}
+        isInvalid={isInvalid}
+        accessibilityHint={accessibilityHint}
       />
-    </View>
+      <Dialog.Outer control={control} testID={testID}>
+        <Dialog.Handle />
+        <Dialog.Inner label={label}>
+          <View style={a.gap_lg}>
+            <View style={[a.relative, a.w_full, a.align_center]}>
+              <DatePicker
+                timeZoneOffsetInMinutes={0}
+                theme={t.name === 'light' ? 'light' : 'dark'}
+                date={new Date(value)}
+                onDateChange={onChangeInternal}
+                mode="date"
+                testID={`${testID}-datepicker`}
+                aria-label={label}
+                accessibilityLabel={label}
+                accessibilityHint={accessibilityHint}
+              />
+            </View>
+            <Button
+              label={_(msg`Done`)}
+              onPress={() => control.close()}
+              size="medium"
+              color="primary"
+              variant="solid">
+              <ButtonText>
+                <Trans>Done</Trans>
+              </ButtonText>
+            </Button>
+          </View>
+        </Dialog.Inner>
+      </Dialog.Outer>
+    </>
   )
 }
diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx
index 32f38a5d1..982d32711 100644
--- a/src/components/forms/DateField/index.web.tsx
+++ b/src/components/forms/DateField/index.web.tsx
@@ -1,11 +1,12 @@
 import React from 'react'
-import {TextInput, TextInputProps, StyleSheet} from 'react-native'
+import {StyleSheet, TextInput, TextInputProps} from 'react-native'
 // @ts-ignore
 import {unstable_createElement} from 'react-native-web'
 
-import * as TextField from '#/components/forms/TextField'
-import {toSimpleDateString} from '#/components/forms/DateField/utils'
 import {DateFieldProps} from '#/components/forms/DateField/types'
+import {toSimpleDateString} from '#/components/forms/DateField/utils'
+import * as TextField from '#/components/forms/TextField'
+import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
 
 export * as utils from '#/components/forms/DateField/utils'
 export const Label = TextField.Label
@@ -37,6 +38,7 @@ export function DateField({
   label,
   isInvalid,
   testID,
+  accessibilityHint,
 }: DateFieldProps) {
   const handleOnChange = React.useCallback(
     (e: any) => {
@@ -52,12 +54,14 @@ export function DateField({
 
   return (
     <TextField.Root isInvalid={isInvalid}>
+      <TextField.Icon icon={CalendarDays} />
       <Input
         value={value}
         label={label}
         onChange={handleOnChange}
         onChangeText={() => {}}
         testID={testID}
+        accessibilityHint={accessibilityHint}
       />
     </TextField.Root>
   )
diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts
index 129f5672d..5400cf903 100644
--- a/src/components/forms/DateField/types.ts
+++ b/src/components/forms/DateField/types.ts
@@ -4,4 +4,5 @@ export type DateFieldProps = {
   label: string
   isInvalid?: boolean
   testID?: string
+  accessibilityHint?: string
 }
diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx
new file mode 100644
index 000000000..9e72df879
--- /dev/null
+++ b/src/components/forms/FormError.tsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import {Text} from '#/components/Typography'
+
+export function FormError({error}: {error?: string}) {
+  const t = useTheme()
+
+  if (!error) return null
+
+  return (
+    <View
+      style={[
+        {backgroundColor: t.palette.negative_400},
+        a.flex_row,
+        a.rounded_sm,
+        a.p_md,
+        a.gap_sm,
+      ]}>
+      <Warning fill={t.palette.white} size="md" />
+      <View>
+        <Text style={[{color: t.palette.white}, a.font_bold, a.leading_snug]}>
+          {error}
+        </Text>
+      </View>
+    </View>
+  )
+}
diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx
new file mode 100644
index 000000000..f2d11062a
--- /dev/null
+++ b/src/components/forms/HostingProvider.tsx
@@ -0,0 +1,95 @@
+import React from 'react'
+import {Keyboard, View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {toNiceDomain} from '#/lib/strings/url-helpers'
+import {isAndroid} from '#/platform/detection'
+import {ServerInputDialog} from '#/view/com/auth/server-input'
+import {atoms as a, useTheme} from '#/alf'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {PencilLine_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
+import {Button} from '../Button'
+import {useDialogControl} from '../Dialog'
+import {Text} from '../Typography'
+
+export function HostingProvider({
+  serviceUrl,
+  onSelectServiceUrl,
+  onOpenDialog,
+}: {
+  serviceUrl: string
+  onSelectServiceUrl: (provider: string) => void
+  onOpenDialog?: () => void
+}) {
+  const serverInputControl = useDialogControl()
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const onPressSelectService = React.useCallback(() => {
+    Keyboard.dismiss()
+    serverInputControl.open()
+    if (onOpenDialog) {
+      onOpenDialog()
+    }
+  }, [onOpenDialog, serverInputControl])
+
+  return (
+    <>
+      <ServerInputDialog
+        control={serverInputControl}
+        onSelect={onSelectServiceUrl}
+      />
+      <Button
+        label={toNiceDomain(serviceUrl)}
+        accessibilityHint={_(msg`Press to change hosting provider`)}
+        variant="solid"
+        color="secondary"
+        style={[
+          a.w_full,
+          a.flex_row,
+          a.align_center,
+          a.rounded_sm,
+          a.px_md,
+          a.pr_sm,
+          a.gap_xs,
+          {paddingVertical: isAndroid ? 14 : 9},
+        ]}
+        onPress={onPressSelectService}>
+        {({hovered, pressed}) => {
+          const interacted = hovered || pressed
+          return (
+            <>
+              <View style={a.pr_xs}>
+                <Globe
+                  size="md"
+                  fill={
+                    interacted ? t.palette.contrast_800 : t.palette.contrast_500
+                  }
+                />
+              </View>
+              <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
+              <View
+                style={[
+                  a.rounded_sm,
+                  interacted
+                    ? t.atoms.bg_contrast_300
+                    : t.atoms.bg_contrast_100,
+                  {marginLeft: 'auto', padding: 6},
+                ]}>
+                <Pencil
+                  size="sm"
+                  style={{
+                    color: interacted
+                      ? t.palette.contrast_800
+                      : t.palette.contrast_500,
+                  }}
+                />
+              </View>
+            </>
+          )
+        }}
+      </Button>
+    </>
+  )
+}
diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index b37f4bfae..376883c9d 100644
--- a/src/components/forms/TextField.tsx
+++ b/src/components/forms/TextField.tsx
@@ -14,6 +14,7 @@ import {useTheme, atoms as a, web, android} from '#/alf'
 import {Text} from '#/components/Typography'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {Props as SVGIconProps} from '#/components/icons/common'
+import {mergeRefs} from '#/lib/merge-refs'
 
 const Context = React.createContext<{
   inputRef: React.RefObject<TextInput> | null
@@ -125,9 +126,10 @@ export function useSharedInputStyles() {
 
 export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
   label: string
-  value: string
-  onChangeText: (value: string) => void
+  value?: string
+  onChangeText?: (value: string) => void
   isInvalid?: boolean
+  inputRef?: React.RefObject<TextInput>
 }
 
 export function createInput(Component: typeof TextInput) {
@@ -137,6 +139,7 @@ export function createInput(Component: typeof TextInput) {
     value,
     onChangeText,
     isInvalid,
+    inputRef,
     ...rest
   }: InputProps) {
     const t = useTheme()
@@ -161,19 +164,22 @@ export function createInput(Component: typeof TextInput) {
       )
     }
 
+    const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
+
     return (
       <>
         <Component
           accessibilityHint={undefined}
           {...rest}
           accessibilityLabel={label}
-          ref={ctx.inputRef}
+          ref={refs}
           value={value}
           onChangeText={onChangeText}
           onFocus={ctx.onFocus}
           onBlur={ctx.onBlur}
           placeholder={placeholder || label}
           placeholderTextColor={t.palette.contrast_500}
+          keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
           hitSlop={HITSLOP_20}
           style={[
             a.relative,
@@ -271,7 +277,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
       <Comp
         size="md"
         style={[
-          {color: t.palette.contrast_500, pointerEvents: 'none'},
+          {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0},
           ctx.hovered ? hover : {},
           ctx.focused ? focus : {},
           ctx.isInvalid && ctx.hovered ? errorHover : {},
diff --git a/src/components/icons/Calendar.tsx b/src/components/icons/Calendar.tsx
new file mode 100644
index 000000000..b3816f28b
--- /dev/null
+++ b/src/components/icons/Calendar.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Calendar_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M8 2a1 1 0 0 1 1 1v1h6V3a1 1 0 1 1 2 0v1h2a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1ZM5 6v3h14V6H5Zm14 5H5v8h14v-8Z',
+})
diff --git a/src/components/icons/Envelope.tsx b/src/components/icons/Envelope.tsx
new file mode 100644
index 000000000..8e40346cd
--- /dev/null
+++ b/src/components/icons/Envelope.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z',
+})
diff --git a/src/components/icons/Lock.tsx b/src/components/icons/Lock.tsx
new file mode 100644
index 000000000..87830b379
--- /dev/null
+++ b/src/components/icons/Lock.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
+})
diff --git a/src/components/icons/Pencil.tsx b/src/components/icons/Pencil.tsx
new file mode 100644
index 000000000..854d51a3b
--- /dev/null
+++ b/src/components/icons/Pencil.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const PencilLine_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z',
+})
diff --git a/src/components/icons/Ticket.tsx b/src/components/icons/Ticket.tsx
new file mode 100644
index 000000000..1a8059c2a
--- /dev/null
+++ b/src/components/icons/Ticket.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
+})
diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts
index a18fef453..bc07b32ec 100644
--- a/src/lib/strings/handles.ts
+++ b/src/lib/strings/handles.ts
@@ -27,6 +27,7 @@ export function sanitizeHandle(handle: string, prefix = ''): string {
 
 export interface IsValidHandle {
   handleChars: boolean
+  hyphenStartOrEnd: boolean
   frontLength: boolean
   totalLength: boolean
   overall: boolean
@@ -39,6 +40,7 @@ export function validateHandle(str: string, userDomain: string): IsValidHandle {
   const results = {
     handleChars:
       !str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')),
+    hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'),
     frontLength: str.length >= 3,
     totalLength: fullHandle.length <= 253,
   }
diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
new file mode 100644
index 000000000..d0d4c784d
--- /dev/null
+++ b/src/screens/Login/ChooseAccountForm.tsx
@@ -0,0 +1,188 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {logEvent} from '#/lib/statsig/statsig'
+import {colors} from '#/lib/styles'
+import {useProfileQuery} from '#/state/queries/profile'
+import {SessionAccount, useSession, useSessionApi} from '#/state/session'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import * as Toast from '#/view/com/util/Toast'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useTheme} from '#/alf'
+import {Button} from '#/components/Button'
+import * as TextField from '#/components/forms/TextField'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+
+function AccountItem({
+  account,
+  onSelect,
+  isCurrentAccount,
+}: {
+  account: SessionAccount
+  onSelect: (account: SessionAccount) => void
+  isCurrentAccount: boolean
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {data: profile} = useProfileQuery({did: account.did})
+
+  const onPress = React.useCallback(() => {
+    onSelect(account)
+  }, [account, onSelect])
+
+  return (
+    <Button
+      testID={`chooseAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[a.flex_1]}
+      onPress={onPress}
+      label={
+        isCurrentAccount
+          ? _(msg`Continue as ${account.handle} (currently signed in)`)
+          : _(msg`Sign in as ${account.handle}`)
+      }>
+      {({hovered, pressed}) => (
+        <View
+          style={[
+            a.flex_1,
+            a.flex_row,
+            a.align_center,
+            {height: 48},
+            (hovered || pressed) && t.atoms.bg_contrast_25,
+          ]}>
+          <View style={a.p_md}>
+            <UserAvatar avatar={profile?.avatar} size={24} />
+          </View>
+          <Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
+            <Text style={[a.font_bold]}>
+              {profile?.displayName || account.handle}{' '}
+            </Text>
+            <Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
+          </Text>
+          {isCurrentAccount ? (
+            <Check size="sm" style={[{color: colors.green3}, a.mr_md]} />
+          ) : (
+            <Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
+          )}
+        </View>
+      )}
+    </Button>
+  )
+}
+export const ChooseAccountForm = ({
+  onSelectAccount,
+  onPressBack,
+}: {
+  onSelectAccount: (account?: SessionAccount) => void
+  onPressBack: () => void
+}) => {
+  const {track, screen} = useAnalytics()
+  const {_} = useLingui()
+  const t = useTheme()
+  const {accounts, currentAccount} = useSession()
+  const {initSession} = useSessionApi()
+  const {setShowLoggedOut} = useLoggedOutViewControls()
+
+  React.useEffect(() => {
+    screen('Choose Account')
+  }, [screen])
+
+  const onSelect = React.useCallback(
+    async (account: SessionAccount) => {
+      if (account.accessJwt) {
+        if (account.did === currentAccount?.did) {
+          setShowLoggedOut(false)
+          Toast.show(_(msg`Already signed in as @${account.handle}`))
+        } else {
+          await initSession(account)
+          logEvent('account:loggedIn', {
+            logContext: 'ChooseAccountForm',
+            withPassword: false,
+          })
+          track('Sign In', {resumedSession: true})
+          setTimeout(() => {
+            Toast.show(_(msg`Signed in as @${account.handle}`))
+          }, 100)
+        }
+      } else {
+        onSelectAccount(account)
+      }
+    },
+    [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _],
+  )
+
+  return (
+    <FormContainer
+      testID="chooseAccountForm"
+      title={<Trans>Select account</Trans>}>
+      <View>
+        <TextField.Label>
+          <Trans>Sign in as...</Trans>
+        </TextField.Label>
+        <View
+          style={[
+            a.rounded_md,
+            a.overflow_hidden,
+            a.border,
+            t.atoms.border_contrast_low,
+          ]}>
+          {accounts.map(account => (
+            <React.Fragment key={account.did}>
+              <AccountItem
+                account={account}
+                onSelect={onSelect}
+                isCurrentAccount={account.did === currentAccount?.did}
+              />
+              <View style={[a.border_b, t.atoms.border_contrast_low]} />
+            </React.Fragment>
+          ))}
+          <Button
+            testID="chooseNewAccountBtn"
+            style={[a.flex_1]}
+            onPress={() => onSelectAccount(undefined)}
+            label={_(msg`Login to account that is not listed`)}>
+            {({hovered, pressed}) => (
+              <View
+                style={[
+                  a.flex_1,
+                  a.flex_row,
+                  a.align_center,
+                  {height: 48},
+                  (hovered || pressed) && t.atoms.bg_contrast_25,
+                ]}>
+                <Text
+                  style={[
+                    a.align_baseline,
+                    a.flex_1,
+                    a.flex_row,
+                    a.py_sm,
+                    {paddingLeft: 48},
+                  ]}>
+                  <Trans>Other account</Trans>
+                </Text>
+                <Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
+              </View>
+            )}
+          </Button>
+        </View>
+      </View>
+      <View style={[a.flex_row]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="medium"
+          onPress={onPressBack}>
+          {_(msg`Back`)}
+        </Button>
+        <View style={[a.flex_1]} />
+      </View>
+    </FormContainer>
+  )
+}
diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx
new file mode 100644
index 000000000..580452e75
--- /dev/null
+++ b/src/screens/Login/ForgotPasswordForm.tsx
@@ -0,0 +1,184 @@
+import React, {useEffect, useState} from 'react'
+import {ActivityIndicator, Keyboard, View} from 'react-native'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
+import {BskyAgent} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import * as EmailValidator from 'email-validator'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {isNetworkError} from '#/lib/strings/errors'
+import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {FormError} from '#/components/forms/FormError'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import * as TextField from '#/components/forms/TextField'
+import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+
+type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
+export const ForgotPasswordForm = ({
+  error,
+  serviceUrl,
+  serviceDescription,
+  setError,
+  setServiceUrl,
+  onPressBack,
+  onEmailSent,
+}: {
+  error: string
+  serviceUrl: string
+  serviceDescription: ServiceDescription | undefined
+  setError: (v: string) => void
+  setServiceUrl: (v: string) => void
+  onPressBack: () => void
+  onEmailSent: () => void
+}) => {
+  const t = useTheme()
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [email, setEmail] = useState<string>('')
+  const {screen} = useAnalytics()
+  const {_} = useLingui()
+
+  useEffect(() => {
+    screen('Signin:ForgotPassword')
+  }, [screen])
+
+  const onPressSelectService = React.useCallback(() => {
+    Keyboard.dismiss()
+  }, [])
+
+  const onPressNext = async () => {
+    if (!EmailValidator.validate(email)) {
+      return setError(_(msg`Your email appears to be invalid.`))
+    }
+
+    setError('')
+    setIsProcessing(true)
+
+    try {
+      const agent = new BskyAgent({service: serviceUrl})
+      await agent.com.atproto.server.requestPasswordReset({email})
+      onEmailSent()
+    } catch (e: any) {
+      const errMsg = e.toString()
+      logger.warn('Failed to request password reset', {error: e})
+      setIsProcessing(false)
+      if (isNetworkError(e)) {
+        setError(
+          _(
+            msg`Unable to contact your service. Please check your Internet connection.`,
+          ),
+        )
+      } else {
+        setError(cleanError(errMsg))
+      }
+    }
+  }
+
+  return (
+    <FormContainer
+      testID="forgotPasswordForm"
+      title={<Trans>Reset password</Trans>}>
+      <View>
+        <TextField.Label>
+          <Trans>Hosting provider</Trans>
+        </TextField.Label>
+        <HostingProvider
+          serviceUrl={serviceUrl}
+          onSelectServiceUrl={setServiceUrl}
+          onOpenDialog={onPressSelectService}
+        />
+      </View>
+      <View>
+        <TextField.Label>
+          <Trans>Email address</Trans>
+        </TextField.Label>
+        <TextField.Root>
+          <TextField.Icon icon={At} />
+          <TextField.Input
+            testID="forgotPasswordEmail"
+            label={_(msg`Enter your email address`)}
+            autoCapitalize="none"
+            autoFocus
+            autoCorrect={false}
+            autoComplete="email"
+            value={email}
+            onChangeText={setEmail}
+            editable={!isProcessing}
+            accessibilityHint={_(msg`Sets email for password reset`)}
+          />
+        </TextField.Root>
+      </View>
+
+      <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
+        <Trans>
+          Enter the email you used to create your account. We'll send you a
+          "reset code" so you can set a new password.
+        </Trans>
+      </Text>
+
+      <FormError error={error} />
+
+      <View style={[a.flex_row, a.align_center, a.pt_md]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="medium"
+          onPress={onPressBack}>
+          <ButtonText>
+            <Trans>Back</Trans>
+          </ButtonText>
+        </Button>
+        <View style={a.flex_1} />
+        {!serviceDescription || isProcessing ? (
+          <ActivityIndicator />
+        ) : (
+          <Button
+            label={_(msg`Next`)}
+            variant="solid"
+            color={'primary'}
+            size="medium"
+            onPress={onPressNext}
+            disabled={!email}>
+            <ButtonText>
+              <Trans>Next</Trans>
+            </ButtonText>
+          </Button>
+        )}
+        {!serviceDescription || isProcessing ? (
+          <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
+            <Trans>Processing...</Trans>
+          </Text>
+        ) : undefined}
+      </View>
+      <View
+        style={[
+          t.atoms.border_contrast_medium,
+          a.border_t,
+          a.pt_2xl,
+          a.mt_md,
+          a.flex_row,
+          a.justify_center,
+        ]}>
+        <Button
+          testID="skipSendEmailButton"
+          onPress={onEmailSent}
+          label={_(msg`Go to next`)}
+          accessibilityHint={_(msg`Navigates to the next screen`)}
+          size="medium"
+          variant="ghost"
+          color="secondary">
+          <ButtonText>
+            <Trans>Already have a code?</Trans>
+          </ButtonText>
+        </Button>
+      </View>
+    </FormContainer>
+  )
+}
diff --git a/src/screens/Login/FormContainer.tsx b/src/screens/Login/FormContainer.tsx
new file mode 100644
index 000000000..e28b48b40
--- /dev/null
+++ b/src/screens/Login/FormContainer.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import {
+  ScrollView,
+  type StyleProp,
+  StyleSheet,
+  View,
+  type ViewStyle,
+} from 'react-native'
+
+import {isWeb} from '#/platform/detection'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+
+export function FormContainer({
+  testID,
+  title,
+  children,
+  style,
+  contentContainerStyle,
+}: {
+  testID?: string
+  title?: React.ReactNode
+  children: React.ReactNode
+  style?: StyleProp<ViewStyle>
+  contentContainerStyle?: StyleProp<ViewStyle>
+}) {
+  const {gtMobile} = useBreakpoints()
+  const t = useTheme()
+  return (
+    <ScrollView
+      testID={testID}
+      style={[styles.maxHeight, contentContainerStyle]}
+      keyboardShouldPersistTaps="handled">
+      <View
+        style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.pt_md], style]}>
+        {title && !gtMobile && (
+          <Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
+            {title}
+          </Text>
+        )}
+        {children}
+      </View>
+    </ScrollView>
+  )
+}
+
+const styles = StyleSheet.create({
+  maxHeight: {
+    // @ts-ignore web only -prf
+    maxHeight: isWeb ? '100vh' : undefined,
+    height: !isWeb ? '100%' : undefined,
+  },
+})
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
new file mode 100644
index 000000000..6bf215ee5
--- /dev/null
+++ b/src/screens/Login/LoginForm.tsx
@@ -0,0 +1,266 @@
+import React, {useRef, useState} from 'react'
+import {
+  ActivityIndicator,
+  Keyboard,
+  LayoutAnimation,
+  TextInput,
+  View,
+} from 'react-native'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {isNetworkError} from '#/lib/strings/errors'
+import {cleanError} from '#/lib/strings/errors'
+import {createFullHandle} from '#/lib/strings/handles'
+import {logger} from '#/logger'
+import {useSessionApi} from '#/state/session'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {FormError} from '#/components/forms/FormError'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import * as TextField from '#/components/forms/TextField'
+import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
+import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+
+type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
+export const LoginForm = ({
+  error,
+  serviceUrl,
+  serviceDescription,
+  initialHandle,
+  setError,
+  setServiceUrl,
+  onPressRetryConnect,
+  onPressBack,
+  onPressForgotPassword,
+}: {
+  error: string
+  serviceUrl: string
+  serviceDescription: ServiceDescription | undefined
+  initialHandle: string
+  setError: (v: string) => void
+  setServiceUrl: (v: string) => void
+  onPressRetryConnect: () => void
+  onPressBack: () => void
+  onPressForgotPassword: () => void
+}) => {
+  const {track} = useAnalytics()
+  const t = useTheme()
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [identifier, setIdentifier] = useState<string>(initialHandle)
+  const [password, setPassword] = useState<string>('')
+  const passwordInputRef = useRef<TextInput>(null)
+  const {_} = useLingui()
+  const {login} = useSessionApi()
+
+  const onPressSelectService = React.useCallback(() => {
+    Keyboard.dismiss()
+    track('Signin:PressedSelectService')
+  }, [track])
+
+  const onPressNext = async () => {
+    if (isProcessing) return
+    Keyboard.dismiss()
+    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+    setError('')
+    setIsProcessing(true)
+
+    try {
+      // try to guess the handle if the user just gave their own username
+      let fullIdent = identifier
+      if (
+        !identifier.includes('@') && // not an email
+        !identifier.includes('.') && // not a domain
+        serviceDescription &&
+        serviceDescription.availableUserDomains.length > 0
+      ) {
+        let matched = false
+        for (const domain of serviceDescription.availableUserDomains) {
+          if (fullIdent.endsWith(domain)) {
+            matched = true
+          }
+        }
+        if (!matched) {
+          fullIdent = createFullHandle(
+            identifier,
+            serviceDescription.availableUserDomains[0],
+          )
+        }
+      }
+
+      // TODO remove double login
+      await login(
+        {
+          service: serviceUrl,
+          identifier: fullIdent,
+          password,
+        },
+        'LoginForm',
+      )
+    } catch (e: any) {
+      const errMsg = e.toString()
+      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+      setIsProcessing(false)
+      if (errMsg.includes('Authentication Required')) {
+        logger.debug('Failed to login due to invalid credentials', {
+          error: errMsg,
+        })
+        setError(_(msg`Invalid username or password`))
+      } else if (isNetworkError(e)) {
+        logger.warn('Failed to login due to network error', {error: errMsg})
+        setError(
+          _(
+            msg`Unable to contact your service. Please check your Internet connection.`,
+          ),
+        )
+      } else {
+        logger.warn('Failed to login', {error: errMsg})
+        setError(cleanError(errMsg))
+      }
+    }
+  }
+
+  const isReady = !!serviceDescription && !!identifier && !!password
+  return (
+    <FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}>
+      <View>
+        <TextField.Label>
+          <Trans>Hosting provider</Trans>
+        </TextField.Label>
+        <HostingProvider
+          serviceUrl={serviceUrl}
+          onSelectServiceUrl={setServiceUrl}
+          onOpenDialog={onPressSelectService}
+        />
+      </View>
+      <View>
+        <TextField.Label>
+          <Trans>Account</Trans>
+        </TextField.Label>
+        <View style={[a.gap_sm]}>
+          <TextField.Root>
+            <TextField.Icon icon={At} />
+            <TextField.Input
+              testID="loginUsernameInput"
+              label={_(msg`Username or email address`)}
+              autoCapitalize="none"
+              autoFocus
+              autoCorrect={false}
+              autoComplete="username"
+              returnKeyType="next"
+              textContentType="username"
+              onSubmitEditing={() => {
+                passwordInputRef.current?.focus()
+              }}
+              blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
+              value={identifier}
+              onChangeText={str =>
+                setIdentifier((str || '').toLowerCase().trim())
+              }
+              editable={!isProcessing}
+              accessibilityHint={_(
+                msg`Input the username or email address you used at signup`,
+              )}
+            />
+          </TextField.Root>
+
+          <TextField.Root>
+            <TextField.Icon icon={Lock} />
+            <TextField.Input
+              testID="loginPasswordInput"
+              inputRef={passwordInputRef}
+              label={_(msg`Password`)}
+              autoCapitalize="none"
+              autoCorrect={false}
+              autoComplete="password"
+              returnKeyType="done"
+              enablesReturnKeyAutomatically={true}
+              secureTextEntry={true}
+              textContentType="password"
+              clearButtonMode="while-editing"
+              value={password}
+              onChangeText={setPassword}
+              onSubmitEditing={onPressNext}
+              blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
+              editable={!isProcessing}
+              accessibilityHint={
+                identifier === ''
+                  ? _(msg`Input your password`)
+                  : _(msg`Input the password tied to ${identifier}`)
+              }
+            />
+            <Button
+              testID="forgotPasswordButton"
+              onPress={onPressForgotPassword}
+              label={_(msg`Forgot password?`)}
+              accessibilityHint={_(msg`Opens password reset form`)}
+              variant="solid"
+              color="secondary"
+              style={[
+                a.rounded_sm,
+                // t.atoms.bg_contrast_100,
+                {marginLeft: 'auto', left: 6, padding: 6},
+                a.z_10,
+              ]}>
+              <ButtonText>
+                <Trans>Forgot?</Trans>
+              </ButtonText>
+            </Button>
+          </TextField.Root>
+        </View>
+      </View>
+      <FormError error={error} />
+      <View style={[a.flex_row, a.align_center, a.pt_md]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="medium"
+          onPress={onPressBack}>
+          <ButtonText>
+            <Trans>Back</Trans>
+          </ButtonText>
+        </Button>
+        <View style={a.flex_1} />
+        {!serviceDescription && error ? (
+          <Button
+            testID="loginRetryButton"
+            label={_(msg`Retry`)}
+            accessibilityHint={_(msg`Retries login`)}
+            variant="solid"
+            color="secondary"
+            size="medium"
+            onPress={onPressRetryConnect}>
+            {_(msg`Retry`)}
+          </Button>
+        ) : !serviceDescription ? (
+          <>
+            <ActivityIndicator />
+            <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
+              <Trans>Connecting...</Trans>
+            </Text>
+          </>
+        ) : isReady ? (
+          <Button
+            label={_(msg`Next`)}
+            accessibilityHint={_(msg`Navigates to the next screen`)}
+            variant="solid"
+            color="primary"
+            size="medium"
+            onPress={onPressNext}>
+            <ButtonText>
+              <Trans>Next</Trans>
+            </ButtonText>
+            {isProcessing && <ButtonIcon icon={Loader} />}
+          </Button>
+        ) : undefined}
+      </View>
+    </FormContainer>
+  )
+}
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
new file mode 100644
index 000000000..5407f3f1e
--- /dev/null
+++ b/src/screens/Login/PasswordUpdatedForm.tsx
@@ -0,0 +1,50 @@
+import React, {useEffect} from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {atoms as a, useBreakpoints} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+
+export const PasswordUpdatedForm = ({
+  onPressNext,
+}: {
+  onPressNext: () => void
+}) => {
+  const {screen} = useAnalytics()
+  const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
+
+  useEffect(() => {
+    screen('Signin:PasswordUpdatedForm')
+  }, [screen])
+
+  return (
+    <FormContainer
+      testID="passwordUpdatedForm"
+      style={[a.gap_2xl, !gtMobile && a.mt_5xl]}>
+      <Text style={[a.text_3xl, a.font_bold, a.text_center]}>
+        <Trans>Password updated!</Trans>
+      </Text>
+      <Text style={[a.text_center, a.mx_auto, {maxWidth: '80%'}]}>
+        <Trans>You can now sign in with your new password.</Trans>
+      </Text>
+      <View style={[a.flex_row, a.justify_center]}>
+        <Button
+          onPress={onPressNext}
+          label={_(msg`Close alert`)}
+          accessibilityHint={_(msg`Closes password update alert`)}
+          variant="solid"
+          color="primary"
+          size="medium">
+          <ButtonText>
+            <Trans>Okay</Trans>
+          </ButtonText>
+        </Button>
+      </View>
+    </FormContainer>
+  )
+}
diff --git a/src/screens/Login/ScreenTransition.tsx b/src/screens/Login/ScreenTransition.tsx
new file mode 100644
index 000000000..ab0a22367
--- /dev/null
+++ b/src/screens/Login/ScreenTransition.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated'
+
+export function ScreenTransition({children}: {children: React.ReactNode}) {
+  return (
+    <Animated.View entering={FadeInRight} exiting={FadeOutLeft}>
+      {children}
+    </Animated.View>
+  )
+}
diff --git a/src/screens/Login/ScreenTransition.web.tsx b/src/screens/Login/ScreenTransition.web.tsx
new file mode 100644
index 000000000..4583720aa
--- /dev/null
+++ b/src/screens/Login/ScreenTransition.web.tsx
@@ -0,0 +1 @@
+export {Fragment as ScreenTransition} from 'react'
diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx
new file mode 100644
index 000000000..e7b488655
--- /dev/null
+++ b/src/screens/Login/SetNewPasswordForm.tsx
@@ -0,0 +1,192 @@
+import React, {useEffect, useState} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {BskyAgent} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {isNetworkError} from '#/lib/strings/errors'
+import {cleanError} from '#/lib/strings/errors'
+import {checkAndFormatResetCode} from '#/lib/strings/password'
+import {logger} from '#/logger'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {FormError} from '#/components/forms/FormError'
+import * as TextField from '#/components/forms/TextField'
+import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
+import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+
+export const SetNewPasswordForm = ({
+  error,
+  serviceUrl,
+  setError,
+  onPressBack,
+  onPasswordSet,
+}: {
+  error: string
+  serviceUrl: string
+  setError: (v: string) => void
+  onPressBack: () => void
+  onPasswordSet: () => void
+}) => {
+  const {screen} = useAnalytics()
+  const {_} = useLingui()
+  const t = useTheme()
+
+  useEffect(() => {
+    screen('Signin:SetNewPasswordForm')
+  }, [screen])
+
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [resetCode, setResetCode] = useState<string>('')
+  const [password, setPassword] = useState<string>('')
+
+  const onPressNext = async () => {
+    // Check that the code is correct. We do this again just incase the user enters the code after their pw and we
+    // don't get to call onBlur first
+    const formattedCode = checkAndFormatResetCode(resetCode)
+    // TODO Better password strength check
+    if (!formattedCode || !password) {
+      setError(
+        _(
+          msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
+        ),
+      )
+      return
+    }
+
+    setError('')
+    setIsProcessing(true)
+
+    try {
+      const agent = new BskyAgent({service: serviceUrl})
+      await agent.com.atproto.server.resetPassword({
+        token: formattedCode,
+        password,
+      })
+      onPasswordSet()
+    } catch (e: any) {
+      const errMsg = e.toString()
+      logger.warn('Failed to set new password', {error: e})
+      setIsProcessing(false)
+      if (isNetworkError(e)) {
+        setError(
+          _(
+            msg`Unable to contact your service. Please check your Internet connection.`,
+          ),
+        )
+      } else {
+        setError(cleanError(errMsg))
+      }
+    }
+  }
+
+  const onBlur = () => {
+    const formattedCode = checkAndFormatResetCode(resetCode)
+    if (!formattedCode) {
+      setError(
+        _(
+          msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
+        ),
+      )
+      return
+    }
+    setResetCode(formattedCode)
+  }
+
+  return (
+    <FormContainer
+      testID="setNewPasswordForm"
+      title={<Trans>Set new password</Trans>}>
+      <Text style={[a.leading_snug, a.mb_sm]}>
+        <Trans>
+          You will receive an email with a "reset code." Enter that code here,
+          then enter your new password.
+        </Trans>
+      </Text>
+
+      <View>
+        <TextField.Label>Reset code</TextField.Label>
+        <TextField.Root>
+          <TextField.Icon icon={Ticket} />
+          <TextField.Input
+            testID="resetCodeInput"
+            label={_(msg`Looks like XXXXX-XXXXX`)}
+            autoCapitalize="none"
+            autoFocus={true}
+            autoCorrect={false}
+            autoComplete="off"
+            value={resetCode}
+            onChangeText={setResetCode}
+            onFocus={() => setError('')}
+            onBlur={onBlur}
+            editable={!isProcessing}
+            accessibilityHint={_(
+              msg`Input code sent to your email for password reset`,
+            )}
+          />
+        </TextField.Root>
+      </View>
+
+      <View>
+        <TextField.Label>New password</TextField.Label>
+        <TextField.Root>
+          <TextField.Icon icon={Lock} />
+          <TextField.Input
+            testID="newPasswordInput"
+            label={_(msg`Enter a password`)}
+            autoCapitalize="none"
+            autoCorrect={false}
+            autoComplete="password"
+            returnKeyType="done"
+            secureTextEntry={true}
+            textContentType="password"
+            clearButtonMode="while-editing"
+            value={password}
+            onChangeText={setPassword}
+            onSubmitEditing={onPressNext}
+            editable={!isProcessing}
+            accessibilityHint={_(msg`Input new password`)}
+          />
+        </TextField.Root>
+      </View>
+
+      <FormError error={error} />
+
+      <View style={[a.flex_row, a.align_center, a.pt_lg]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="medium"
+          onPress={onPressBack}>
+          <ButtonText>
+            <Trans>Back</Trans>
+          </ButtonText>
+        </Button>
+        <View style={a.flex_1} />
+        {isProcessing ? (
+          <ActivityIndicator />
+        ) : (
+          <Button
+            label={_(msg`Next`)}
+            variant="solid"
+            color="primary"
+            size="medium"
+            onPress={onPressNext}>
+            <ButtonText>
+              <Trans>Next</Trans>
+            </ButtonText>
+          </Button>
+        )}
+        {isProcessing ? (
+          <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
+            <Trans>Updating...</Trans>
+          </Text>
+        ) : undefined}
+      </View>
+    </FormContainer>
+  )
+}
diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx
new file mode 100644
index 000000000..49f7518b0
--- /dev/null
+++ b/src/screens/Login/index.tsx
@@ -0,0 +1,174 @@
+import React from 'react'
+import {KeyboardAvoidingView} from 'react-native'
+import {LayoutAnimationConfig} from 'react-native-reanimated'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {DEFAULT_SERVICE} from '#/lib/constants'
+import {logger} from '#/logger'
+import {useServiceQuery} from '#/state/queries/service'
+import {SessionAccount, useSession} from '#/state/session'
+import {useLoggedOutView} from '#/state/shell/logged-out'
+import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
+import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
+import {LoginForm} from '#/screens/Login/LoginForm'
+import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
+import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
+import {atoms as a} from '#/alf'
+import {ChooseAccountForm} from './ChooseAccountForm'
+import {ScreenTransition} from './ScreenTransition'
+
+enum Forms {
+  Login,
+  ChooseAccount,
+  ForgotPassword,
+  SetNewPassword,
+  PasswordUpdated,
+}
+
+export const Login = ({onPressBack}: {onPressBack: () => void}) => {
+  const {_} = useLingui()
+
+  const {accounts} = useSession()
+  const {track} = useAnalytics()
+  const {requestedAccountSwitchTo} = useLoggedOutView()
+  const requestedAccount = accounts.find(
+    acc => acc.did === requestedAccountSwitchTo,
+  )
+
+  const [error, setError] = React.useState<string>('')
+  const [serviceUrl, setServiceUrl] = React.useState<string>(
+    requestedAccount?.service || DEFAULT_SERVICE,
+  )
+  const [initialHandle, setInitialHandle] = React.useState<string>(
+    requestedAccount?.handle || '',
+  )
+  const [currentForm, setCurrentForm] = React.useState<Forms>(
+    requestedAccount
+      ? Forms.Login
+      : accounts.length
+      ? Forms.ChooseAccount
+      : Forms.Login,
+  )
+
+  const {
+    data: serviceDescription,
+    error: serviceError,
+    refetch: refetchService,
+  } = useServiceQuery(serviceUrl)
+
+  const onSelectAccount = (account?: SessionAccount) => {
+    if (account?.service) {
+      setServiceUrl(account.service)
+    }
+    setInitialHandle(account?.handle || '')
+    setCurrentForm(Forms.Login)
+  }
+
+  const gotoForm = (form: Forms) => {
+    setError('')
+    setCurrentForm(form)
+  }
+
+  React.useEffect(() => {
+    if (serviceError) {
+      setError(
+        _(
+          msg`Unable to contact your service. Please check your Internet connection.`,
+        ),
+      )
+      logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
+        error: String(serviceError),
+      })
+    } else {
+      setError('')
+    }
+  }, [serviceError, serviceUrl, _])
+
+  const onPressForgotPassword = () => {
+    track('Signin:PressedForgotPassword')
+    setCurrentForm(Forms.ForgotPassword)
+  }
+
+  let content = null
+  let title = ''
+  let description = ''
+
+  switch (currentForm) {
+    case Forms.Login:
+      title = _(msg`Sign in`)
+      description = _(msg`Enter your username and password`)
+      content = (
+        <LoginForm
+          error={error}
+          serviceUrl={serviceUrl}
+          serviceDescription={serviceDescription}
+          initialHandle={initialHandle}
+          setError={setError}
+          setServiceUrl={setServiceUrl}
+          onPressBack={() =>
+            accounts.length ? gotoForm(Forms.ChooseAccount) : onPressBack()
+          }
+          onPressForgotPassword={onPressForgotPassword}
+          onPressRetryConnect={refetchService}
+        />
+      )
+      break
+    case Forms.ChooseAccount:
+      title = _(msg`Sign in`)
+      description = _(msg`Select from an existing account`)
+      content = (
+        <ChooseAccountForm
+          onSelectAccount={onSelectAccount}
+          onPressBack={onPressBack}
+        />
+      )
+      break
+    case Forms.ForgotPassword:
+      title = _(msg`Forgot Password`)
+      description = _(msg`Let's get your password reset!`)
+      content = (
+        <ForgotPasswordForm
+          error={error}
+          serviceUrl={serviceUrl}
+          serviceDescription={serviceDescription}
+          setError={setError}
+          setServiceUrl={setServiceUrl}
+          onPressBack={() => gotoForm(Forms.Login)}
+          onEmailSent={() => gotoForm(Forms.SetNewPassword)}
+        />
+      )
+      break
+    case Forms.SetNewPassword:
+      title = _(msg`Forgot Password`)
+      description = _(msg`Let's get your password reset!`)
+      content = (
+        <SetNewPasswordForm
+          error={error}
+          serviceUrl={serviceUrl}
+          setError={setError}
+          onPressBack={() => gotoForm(Forms.ForgotPassword)}
+          onPasswordSet={() => gotoForm(Forms.PasswordUpdated)}
+        />
+      )
+      break
+    case Forms.PasswordUpdated:
+      title = _(msg`Password updated`)
+      description = _(msg`You can now sign in with your new password.`)
+      content = (
+        <PasswordUpdatedForm onPressNext={() => gotoForm(Forms.Login)} />
+      )
+      break
+  }
+
+  return (
+    <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}>
+      <LoggedOutLayout leadin="" title={title} description={description}>
+        <LayoutAnimationConfig skipEntering skipExiting>
+          <ScreenTransition key={currentForm}>{content}</ScreenTransition>
+        </LayoutAnimationConfig>
+      </LoggedOutLayout>
+    </KeyboardAvoidingView>
+  )
+}
diff --git a/src/view/com/auth/create/CaptchaWebView.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
index b0de8b4a4..50918c4ce 100644
--- a/src/view/com/auth/create/CaptchaWebView.tsx
+++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
@@ -1,8 +1,9 @@
 import React from 'react'
+import {StyleSheet} from 'react-native'
 import {WebView, WebViewNavigation} from 'react-native-webview'
 import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
-import {StyleSheet} from 'react-native'
-import {CreateAccountState} from 'view/com/auth/create/state'
+
+import {SignupState} from '#/screens/Signup/state'
 
 const ALLOWED_HOSTS = [
   'bsky.social',
@@ -17,24 +18,24 @@ const ALLOWED_HOSTS = [
 export function CaptchaWebView({
   url,
   stateParam,
-  uiState,
+  state,
   onSuccess,
   onError,
 }: {
   url: string
   stateParam: string
-  uiState?: CreateAccountState
+  state?: SignupState
   onSuccess: (code: string) => void
   onError: () => void
 }) {
   const redirectHost = React.useMemo(() => {
-    if (!uiState?.serviceUrl) return 'bsky.app'
+    if (!state?.serviceUrl) return 'bsky.app'
 
-    return uiState?.serviceUrl &&
-      new URL(uiState?.serviceUrl).host === 'staging.bsky.dev'
+    return state?.serviceUrl &&
+      new URL(state?.serviceUrl).host === 'staging.bsky.dev'
       ? 'staging.bsky.app'
       : 'bsky.app'
-  }, [uiState?.serviceUrl])
+  }, [state?.serviceUrl])
 
   const wasSuccessful = React.useRef(false)
 
diff --git a/src/view/com/auth/create/CaptchaWebView.web.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx
index 7791a58dd..7791a58dd 100644
--- a/src/view/com/auth/create/CaptchaWebView.web.tsx
+++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx
diff --git a/src/screens/Signup/StepCaptcha/index.tsx b/src/screens/Signup/StepCaptcha/index.tsx
new file mode 100644
index 000000000..311c697e7
--- /dev/null
+++ b/src/screens/Signup/StepCaptcha/index.tsx
@@ -0,0 +1,95 @@
+import React from 'react'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {nanoid} from 'nanoid/non-secure'
+
+import {createFullHandle} from '#/lib/strings/handles'
+import {isWeb} from '#/platform/detection'
+import {ScreenTransition} from '#/screens/Login/ScreenTransition'
+import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state'
+import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
+import {atoms as a, useTheme} from '#/alf'
+import {FormError} from '#/components/forms/FormError'
+
+const CAPTCHA_PATH = '/gate/signup'
+
+export function StepCaptcha() {
+  const {_} = useLingui()
+  const theme = useTheme()
+  const {state, dispatch} = useSignupContext()
+  const submit = useSubmitSignup({state, dispatch})
+
+  const [completed, setCompleted] = React.useState(false)
+
+  const stateParam = React.useMemo(() => nanoid(15), [])
+  const url = React.useMemo(() => {
+    const newUrl = new URL(state.serviceUrl)
+    newUrl.pathname = CAPTCHA_PATH
+    newUrl.searchParams.set(
+      'handle',
+      createFullHandle(state.handle, state.userDomain),
+    )
+    newUrl.searchParams.set('state', stateParam)
+    newUrl.searchParams.set('colorScheme', theme.name)
+
+    return newUrl.href
+  }, [state.serviceUrl, state.handle, state.userDomain, stateParam, theme.name])
+
+  const onSuccess = React.useCallback(
+    (code: string) => {
+      setCompleted(true)
+      submit(code)
+    },
+    [submit],
+  )
+
+  const onError = React.useCallback(() => {
+    dispatch({
+      type: 'setError',
+      value: _(msg`Error receiving captcha response.`),
+    })
+  }, [_, dispatch])
+
+  return (
+    <ScreenTransition>
+      <View style={[a.gap_lg]}>
+        <View style={[styles.container, completed && styles.center]}>
+          {!completed ? (
+            <CaptchaWebView
+              url={url}
+              stateParam={stateParam}
+              state={state}
+              onSuccess={onSuccess}
+              onError={onError}
+            />
+          ) : (
+            <ActivityIndicator size="large" />
+          )}
+        </View>
+        <FormError error={state.error} />
+      </View>
+    </ScreenTransition>
+  )
+}
+
+const styles = StyleSheet.create({
+  error: {
+    borderRadius: 6,
+    marginTop: 10,
+  },
+  // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
+  touchable: {
+    ...(isWeb && {cursor: 'pointer'}),
+  },
+  container: {
+    minHeight: 500,
+    width: '100%',
+    paddingBottom: 20,
+    overflow: 'hidden',
+  },
+  center: {
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+})
diff --git a/src/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx
new file mode 100644
index 000000000..44a33b833
--- /dev/null
+++ b/src/screens/Signup/StepHandle.tsx
@@ -0,0 +1,134 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect} from '@react-navigation/native'
+
+import {
+  createFullHandle,
+  IsValidHandle,
+  validateHandle,
+} from '#/lib/strings/handles'
+import {ScreenTransition} from '#/screens/Login/ScreenTransition'
+import {useSignupContext} from '#/screens/Signup/state'
+import {atoms as a, useTheme} from '#/alf'
+import * as TextField from '#/components/forms/TextField'
+import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
+import {Text} from '#/components/Typography'
+
+export function StepHandle() {
+  const {_} = useLingui()
+  const t = useTheme()
+  const {state, dispatch} = useSignupContext()
+
+  const [validCheck, setValidCheck] = React.useState<IsValidHandle>({
+    handleChars: false,
+    hyphenStartOrEnd: false,
+    frontLength: false,
+    totalLength: true,
+    overall: false,
+  })
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setValidCheck(validateHandle(state.handle, state.userDomain))
+    }, [state.handle, state.userDomain]),
+  )
+
+  const onHandleChange = React.useCallback(
+    (value: string) => {
+      if (state.error) {
+        dispatch({type: 'setError', value: ''})
+      }
+
+      dispatch({
+        type: 'setHandle',
+        value,
+      })
+    },
+    [dispatch, state.error],
+  )
+
+  return (
+    <ScreenTransition>
+      <View style={[a.gap_lg]}>
+        <View>
+          <TextField.Root>
+            <TextField.Icon icon={At} />
+            <TextField.Input
+              onChangeText={onHandleChange}
+              label={_(msg`Input your user handle`)}
+              defaultValue={state.handle}
+              autoCapitalize="none"
+              autoCorrect={false}
+              autoFocus
+              autoComplete="off"
+            />
+          </TextField.Root>
+        </View>
+        <Text style={[a.text_md]}>
+          <Trans>Your full handle will be</Trans>{' '}
+          <Text style={[a.text_md, a.font_bold]}>
+            @{createFullHandle(state.handle, state.userDomain)}
+          </Text>
+        </Text>
+
+        <View
+          style={[
+            a.w_full,
+            a.rounded_sm,
+            a.border,
+            a.p_md,
+            a.gap_sm,
+            t.atoms.border_contrast_low,
+          ]}>
+          {state.error ? (
+            <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
+              <IsValidIcon valid={false} />
+              <Text style={[a.text_md, a.flex_1]}>{state.error}</Text>
+            </View>
+          ) : undefined}
+          {validCheck.hyphenStartOrEnd ? (
+            <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
+              <IsValidIcon valid={validCheck.handleChars} />
+              <Text style={[a.text_md, a.flex_1]}>
+                <Trans>Only contains letters, numbers, and hyphens</Trans>
+              </Text>
+            </View>
+          ) : (
+            <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
+              <IsValidIcon valid={validCheck.hyphenStartOrEnd} />
+              <Text style={[a.text_md, a.flex_1]}>
+                <Trans>Doesn't begin or end with a hyphen</Trans>
+              </Text>
+            </View>
+          )}
+          <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
+            <IsValidIcon
+              valid={validCheck.frontLength && validCheck.totalLength}
+            />
+            {!validCheck.totalLength ? (
+              <Text style={[a.text_md, a.flex_1]}>
+                <Trans>No longer than 253 characters</Trans>
+              </Text>
+            ) : (
+              <Text style={[a.text_md, a.flex_1]}>
+                <Trans>At least 3 characters</Trans>
+              </Text>
+            )}
+          </View>
+        </View>
+      </View>
+    </ScreenTransition>
+  )
+}
+
+function IsValidIcon({valid}: {valid: boolean}) {
+  const t = useTheme()
+  if (!valid) {
+    return <Times size="md" style={{color: t.palette.negative_500}} />
+  }
+  return <Check size="md" style={{color: t.palette.positive_700}} />
+}
diff --git a/src/screens/Signup/StepInfo/Policies.tsx b/src/screens/Signup/StepInfo/Policies.tsx
new file mode 100644
index 000000000..8a656203f
--- /dev/null
+++ b/src/screens/Signup/StepInfo/Policies.tsx
@@ -0,0 +1,97 @@
+import React from 'react'
+import {View} from 'react-native'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {atoms as a, useTheme} from '#/alf'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {InlineLink} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+export const Policies = ({
+  serviceDescription,
+  needsGuardian,
+  under13,
+}: {
+  serviceDescription: ComAtprotoServerDescribeServer.OutputSchema
+  needsGuardian: boolean
+  under13: boolean
+}) => {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  if (!serviceDescription) {
+    return <View />
+  }
+
+  const tos = validWebLink(serviceDescription.links?.termsOfService)
+  const pp = validWebLink(serviceDescription.links?.privacyPolicy)
+
+  if (!tos && !pp) {
+    return (
+      <View style={[a.flex_row, a.align_center, a.gap_xs]}>
+        <CircleInfo size="md" fill={t.atoms.text_contrast_low.color} />
+
+        <Text style={[t.atoms.text_contrast_medium]}>
+          <Trans>
+            This service has not provided terms of service or a privacy policy.
+          </Trans>
+        </Text>
+      </View>
+    )
+  }
+
+  const els = []
+  if (tos) {
+    els.push(
+      <InlineLink key="tos" to={tos}>
+        {_(msg`Terms of Service`)}
+      </InlineLink>,
+    )
+  }
+  if (pp) {
+    els.push(
+      <InlineLink key="pp" to={pp}>
+        {_(msg`Privacy Policy`)}
+      </InlineLink>,
+    )
+  }
+  if (els.length === 2) {
+    els.splice(
+      1,
+      0,
+      <Text key="and" style={[t.atoms.text_contrast_medium]}>
+        {' '}
+        and{' '}
+      </Text>,
+    )
+  }
+
+  return (
+    <View style={[a.gap_sm]}>
+      <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
+        <Trans>By creating an account you agree to the {els}.</Trans>
+      </Text>
+
+      {under13 ? (
+        <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
+          You must be 13 years of age or older to sign up.
+        </Text>
+      ) : needsGuardian ? (
+        <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
+          <Trans>
+            If you are not yet an adult according to the laws of your country,
+            your parent or legal guardian must read these Terms on your behalf.
+          </Trans>
+        </Text>
+      ) : undefined}
+    </View>
+  )
+}
+
+function validWebLink(url?: string): string | undefined {
+  return url && (url.startsWith('http://') || url.startsWith('https://'))
+    ? url
+    : undefined
+}
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
new file mode 100644
index 000000000..136592a0b
--- /dev/null
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -0,0 +1,146 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {ScreenTransition} from '#/screens/Login/ScreenTransition'
+import {is13, is18, useSignupContext} from '#/screens/Signup/state'
+import {Policies} from '#/screens/Signup/StepInfo/Policies'
+import {atoms as a} from '#/alf'
+import * as DateField from '#/components/forms/DateField'
+import {FormError} from '#/components/forms/FormError'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import * as TextField from '#/components/forms/TextField'
+import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
+import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
+import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
+import {Loader} from '#/components/Loader'
+
+function sanitizeDate(date: Date): Date {
+  if (!date || date.toString() === 'Invalid Date') {
+    logger.error(`Create account: handled invalid date for birthDate`, {
+      hasDate: !!date,
+    })
+    return new Date()
+  }
+  return date
+}
+
+export function StepInfo() {
+  const {_} = useLingui()
+  const {state, dispatch} = useSignupContext()
+
+  return (
+    <ScreenTransition>
+      <View style={[a.gap_md]}>
+        <FormError error={state.error} />
+        <View>
+          <TextField.Label>
+            <Trans>Hosting provider</Trans>
+          </TextField.Label>
+          <HostingProvider
+            serviceUrl={state.serviceUrl}
+            onSelectServiceUrl={v =>
+              dispatch({type: 'setServiceUrl', value: v})
+            }
+          />
+        </View>
+        {state.isLoading ? (
+          <View style={[a.align_center]}>
+            <Loader size="xl" />
+          </View>
+        ) : state.serviceDescription ? (
+          <>
+            {state.serviceDescription.inviteCodeRequired && (
+              <View>
+                <TextField.Label>
+                  <Trans>Invite code</Trans>
+                </TextField.Label>
+                <TextField.Root>
+                  <TextField.Icon icon={Ticket} />
+                  <TextField.Input
+                    onChangeText={value => {
+                      dispatch({
+                        type: 'setInviteCode',
+                        value: value.trim(),
+                      })
+                    }}
+                    label={_(msg`Required for this provider`)}
+                    defaultValue={state.inviteCode}
+                    autoCapitalize="none"
+                    autoComplete="email"
+                    keyboardType="email-address"
+                  />
+                </TextField.Root>
+              </View>
+            )}
+            <View>
+              <TextField.Label>
+                <Trans>Email</Trans>
+              </TextField.Label>
+              <TextField.Root>
+                <TextField.Icon icon={Envelope} />
+                <TextField.Input
+                  onChangeText={value => {
+                    dispatch({
+                      type: 'setEmail',
+                      value: value.trim(),
+                    })
+                  }}
+                  label={_(msg`Enter your email address`)}
+                  defaultValue={state.email}
+                  autoCapitalize="none"
+                  autoComplete="email"
+                  keyboardType="email-address"
+                />
+              </TextField.Root>
+            </View>
+            <View>
+              <TextField.Label>
+                <Trans>Password</Trans>
+              </TextField.Label>
+              <TextField.Root>
+                <TextField.Icon icon={Lock} />
+                <TextField.Input
+                  onChangeText={value => {
+                    dispatch({
+                      type: 'setPassword',
+                      value,
+                    })
+                  }}
+                  label={_(msg`Choose your password`)}
+                  defaultValue={state.password}
+                  secureTextEntry
+                  autoComplete="new-password"
+                />
+              </TextField.Root>
+            </View>
+            <View>
+              <DateField.Label>
+                <Trans>Your birth date</Trans>
+              </DateField.Label>
+              <DateField.DateField
+                testID="date"
+                value={DateField.utils.toSimpleDateString(state.dateOfBirth)}
+                onChangeDate={date => {
+                  dispatch({
+                    type: 'setDateOfBirth',
+                    value: sanitizeDate(new Date(date)),
+                  })
+                }}
+                label={_(msg`Date of birth`)}
+                accessibilityHint={_(msg`Select your date of birth`)}
+              />
+            </View>
+            <Policies
+              serviceDescription={state.serviceDescription}
+              needsGuardian={!is18(state.dateOfBirth)}
+              under13={!is13(state.dateOfBirth)}
+            />
+          </>
+        ) : undefined}
+      </View>
+    </ScreenTransition>
+  )
+}
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
new file mode 100644
index 000000000..f19823b4f
--- /dev/null
+++ b/src/screens/Signup/index.tsx
@@ -0,0 +1,211 @@
+import React from 'react'
+import {ScrollView, View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {FEEDBACK_FORM_URL} from '#/lib/constants'
+import {createFullHandle} from '#/lib/strings/handles'
+import {useServiceQuery} from '#/state/queries/service'
+import {getAgent} from '#/state/session'
+import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
+import {
+  initialState,
+  reducer,
+  SignupContext,
+  SignupStep,
+  useSubmitSignup,
+} from '#/screens/Signup/state'
+import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
+import {StepHandle} from '#/screens/Signup/StepHandle'
+import {StepInfo} from '#/screens/Signup/StepInfo'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {Divider} from '#/components/Divider'
+import {InlineLink} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+export function Signup({onPressBack}: {onPressBack: () => void}) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const {screen} = useAnalytics()
+  const [state, dispatch] = React.useReducer(reducer, initialState)
+  const submit = useSubmitSignup({state, dispatch})
+
+  const {
+    data: serviceInfo,
+    isFetching,
+    isError,
+    refetch,
+  } = useServiceQuery(state.serviceUrl)
+
+  React.useEffect(() => {
+    screen('CreateAccount')
+  }, [screen])
+
+  React.useEffect(() => {
+    if (isFetching) {
+      dispatch({type: 'setIsLoading', value: true})
+    } else if (!isFetching) {
+      dispatch({type: 'setIsLoading', value: false})
+    }
+  }, [isFetching])
+
+  React.useEffect(() => {
+    if (isError) {
+      dispatch({type: 'setServiceDescription', value: undefined})
+      dispatch({
+        type: 'setError',
+        value: _(
+          msg`Unable to contact your service. Please check your Internet connection.`,
+        ),
+      })
+    } else if (serviceInfo) {
+      dispatch({type: 'setServiceDescription', value: serviceInfo})
+      dispatch({type: 'setError', value: ''})
+    }
+  }, [_, serviceInfo, isError])
+
+  const onNextPress = React.useCallback(async () => {
+    if (state.activeStep === SignupStep.HANDLE) {
+      try {
+        dispatch({type: 'setIsLoading', value: true})
+
+        const res = await getAgent().resolveHandle({
+          handle: createFullHandle(state.handle, state.userDomain),
+        })
+
+        if (res.data.did) {
+          dispatch({
+            type: 'setError',
+            value: _(msg`That handle is already taken.`),
+          })
+          return
+        }
+      } catch (e) {
+        // Don't have to handle
+      } finally {
+        dispatch({type: 'setIsLoading', value: false})
+      }
+    }
+
+    // phoneVerificationRequired is actually whether a captcha is required
+    if (
+      state.activeStep === SignupStep.HANDLE &&
+      !state.serviceDescription?.phoneVerificationRequired
+    ) {
+      submit()
+      return
+    }
+
+    dispatch({type: 'next'})
+  }, [
+    _,
+    state.activeStep,
+    state.handle,
+    state.serviceDescription?.phoneVerificationRequired,
+    state.userDomain,
+    submit,
+  ])
+
+  const onBackPress = React.useCallback(() => {
+    if (state.activeStep !== SignupStep.INFO) {
+      dispatch({type: 'prev'})
+    } else {
+      onPressBack()
+    }
+  }, [onPressBack, state.activeStep])
+
+  return (
+    <SignupContext.Provider value={{state, dispatch}}>
+      <LoggedOutLayout
+        leadin=""
+        title={_(msg`Create Account`)}
+        description={_(msg`We're so excited to have you join us!`)}>
+        <ScrollView
+          testID="createAccount"
+          keyboardShouldPersistTaps="handled"
+          style={a.h_full}
+          keyboardDismissMode="on-drag">
+          <View style={[a.flex_1, a.px_xl, a.pt_2xl, {paddingBottom: 100}]}>
+            <View style={[a.gap_sm, a.pb_3xl]}>
+              <Text style={[a.font_semibold, t.atoms.text_contrast_medium]}>
+                <Trans>Step</Trans> {state.activeStep + 1} <Trans>of</Trans>{' '}
+                {state.serviceDescription &&
+                !state.serviceDescription.phoneVerificationRequired
+                  ? '2'
+                  : '3'}
+              </Text>
+              <Text style={[a.text_3xl, a.font_bold]}>
+                {state.activeStep === SignupStep.INFO ? (
+                  <Trans>Your account</Trans>
+                ) : state.activeStep === SignupStep.HANDLE ? (
+                  <Trans>Your user handle</Trans>
+                ) : (
+                  <Trans>Complete the challenge</Trans>
+                )}
+              </Text>
+            </View>
+
+            <View style={[a.pb_3xl]}>
+              {state.activeStep === SignupStep.INFO ? (
+                <StepInfo />
+              ) : state.activeStep === SignupStep.HANDLE ? (
+                <StepHandle />
+              ) : (
+                <StepCaptcha />
+              )}
+            </View>
+
+            <View style={[a.flex_row, a.justify_between, a.pb_lg]}>
+              <Button
+                label="Back"
+                variant="solid"
+                color="secondary"
+                size="medium"
+                onPress={onBackPress}>
+                Back
+              </Button>
+              {state.activeStep !== SignupStep.CAPTCHA && (
+                <>
+                  {isError ? (
+                    <Button
+                      label="Retry"
+                      variant="solid"
+                      color="primary"
+                      size="medium"
+                      disabled={state.isLoading}
+                      onPress={() => refetch()}>
+                      Retry
+                    </Button>
+                  ) : (
+                    <Button
+                      label="Next"
+                      variant="solid"
+                      color="primary"
+                      size="medium"
+                      disabled={!state.canNext || state.isLoading}
+                      onPress={onNextPress}>
+                      <ButtonText>Next</ButtonText>
+                    </Button>
+                  )}
+                </>
+              )}
+            </View>
+
+            <Divider />
+
+            <View style={[a.w_full, a.py_lg]}>
+              <Text style={[t.atoms.text_contrast_medium]}>
+                <Trans>Having trouble?</Trans>{' '}
+                <InlineLink to={FEEDBACK_FORM_URL({email: state.email})}>
+                  <Trans>Contact support</Trans>
+                </InlineLink>
+              </Text>
+            </View>
+          </View>
+        </ScrollView>
+      </LoggedOutLayout>
+    </SignupContext.Provider>
+  )
+}
diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts
new file mode 100644
index 000000000..f185e2d44
--- /dev/null
+++ b/src/screens/Signup/state.ts
@@ -0,0 +1,320 @@
+import React, {useCallback} from 'react'
+import {LayoutAnimation} from 'react-native'
+import {
+  ComAtprotoServerCreateAccount,
+  ComAtprotoServerDescribeServer,
+} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import * as EmailValidator from 'email-validator'
+
+import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants'
+import {cleanError} from '#/lib/strings/errors'
+import {createFullHandle, validateHandle} from '#/lib/strings/handles'
+import {getAge} from '#/lib/strings/time'
+import {logger} from '#/logger'
+import {
+  DEFAULT_PROD_FEEDS,
+  usePreferencesSetBirthDateMutation,
+  useSetSaveFeedsMutation,
+} from '#/state/queries/preferences'
+import {useSessionApi} from '#/state/session'
+import {useOnboardingDispatch} from '#/state/shell'
+
+export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
+const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
+
+export enum SignupStep {
+  INFO,
+  HANDLE,
+  CAPTCHA,
+}
+
+export type SignupState = {
+  hasPrev: boolean
+  canNext: boolean
+  activeStep: SignupStep
+
+  serviceUrl: string
+  serviceDescription?: ServiceDescription
+  userDomain: string
+  dateOfBirth: Date
+  email: string
+  password: string
+  inviteCode: string
+  handle: string
+
+  error: string
+  isLoading: boolean
+}
+
+export type SignupAction =
+  | {type: 'prev'}
+  | {type: 'next'}
+  | {type: 'finish'}
+  | {type: 'setStep'; value: SignupStep}
+  | {type: 'setServiceUrl'; value: string}
+  | {type: 'setServiceDescription'; value: ServiceDescription | undefined}
+  | {type: 'setEmail'; value: string}
+  | {type: 'setPassword'; value: string}
+  | {type: 'setDateOfBirth'; value: Date}
+  | {type: 'setInviteCode'; value: string}
+  | {type: 'setHandle'; value: string}
+  | {type: 'setVerificationCode'; value: string}
+  | {type: 'setError'; value: string}
+  | {type: 'setCanNext'; value: boolean}
+  | {type: 'setIsLoading'; value: boolean}
+
+export const initialState: SignupState = {
+  hasPrev: false,
+  canNext: false,
+  activeStep: SignupStep.INFO,
+
+  serviceUrl: DEFAULT_SERVICE,
+  serviceDescription: undefined,
+  userDomain: '',
+  dateOfBirth: DEFAULT_DATE,
+  email: '',
+  password: '',
+  handle: '',
+  inviteCode: '',
+
+  error: '',
+  isLoading: false,
+}
+
+export function is13(date: Date) {
+  return getAge(date) >= 13
+}
+
+export function is18(date: Date) {
+  return getAge(date) >= 18
+}
+
+export function reducer(s: SignupState, a: SignupAction): SignupState {
+  let next = {...s}
+
+  switch (a.type) {
+    case 'prev': {
+      if (s.activeStep !== SignupStep.INFO) {
+        LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+        next.activeStep--
+        next.error = ''
+      }
+      break
+    }
+    case 'next': {
+      if (s.activeStep !== SignupStep.CAPTCHA) {
+        LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+        next.activeStep++
+        next.error = ''
+      }
+      break
+    }
+    case 'setStep': {
+      next.activeStep = a.value
+      break
+    }
+    case 'setServiceUrl': {
+      next.serviceUrl = a.value
+      break
+    }
+    case 'setServiceDescription': {
+      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+
+      next.serviceDescription = a.value
+      next.userDomain = a.value?.availableUserDomains[0] ?? ''
+      next.isLoading = false
+      break
+    }
+
+    case 'setEmail': {
+      next.email = a.value
+      break
+    }
+    case 'setPassword': {
+      next.password = a.value
+      break
+    }
+    case 'setDateOfBirth': {
+      next.dateOfBirth = a.value
+      break
+    }
+    case 'setInviteCode': {
+      next.inviteCode = a.value
+      break
+    }
+    case 'setHandle': {
+      next.handle = a.value
+      break
+    }
+    case 'setCanNext': {
+      next.canNext = a.value
+      break
+    }
+    case 'setIsLoading': {
+      next.isLoading = a.value
+      break
+    }
+    case 'setError': {
+      next.error = a.value
+      break
+    }
+  }
+
+  next.hasPrev = next.activeStep !== SignupStep.INFO
+
+  switch (next.activeStep) {
+    case SignupStep.INFO: {
+      const isValidEmail = EmailValidator.validate(next.email)
+      next.canNext =
+        !!(next.email && next.password && next.dateOfBirth) &&
+        (!next.serviceDescription?.inviteCodeRequired || !!next.inviteCode) &&
+        is13(next.dateOfBirth) &&
+        isValidEmail
+      break
+    }
+    case SignupStep.HANDLE: {
+      next.canNext =
+        !!next.handle && validateHandle(next.handle, next.userDomain).overall
+      break
+    }
+  }
+
+  logger.debug('signup', next)
+
+  if (s.activeStep !== next.activeStep) {
+    logger.debug('signup: step changed', {activeStep: next.activeStep})
+  }
+
+  return next
+}
+
+interface IContext {
+  state: SignupState
+  dispatch: React.Dispatch<SignupAction>
+}
+export const SignupContext = React.createContext<IContext>({} as IContext)
+export const useSignupContext = () => React.useContext(SignupContext)
+
+export function useSubmitSignup({
+  state,
+  dispatch,
+}: {
+  state: SignupState
+  dispatch: (action: SignupAction) => void
+}) {
+  const {_} = useLingui()
+  const {createAccount} = useSessionApi()
+  const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
+  const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
+  const onboardingDispatch = useOnboardingDispatch()
+
+  return useCallback(
+    async (verificationCode?: string) => {
+      if (!state.email) {
+        dispatch({type: 'setStep', value: SignupStep.INFO})
+        return dispatch({
+          type: 'setError',
+          value: _(msg`Please enter your email.`),
+        })
+      }
+      if (!EmailValidator.validate(state.email)) {
+        dispatch({type: 'setStep', value: SignupStep.INFO})
+        return dispatch({
+          type: 'setError',
+          value: _(msg`Your email appears to be invalid.`),
+        })
+      }
+      if (!state.password) {
+        dispatch({type: 'setStep', value: SignupStep.INFO})
+        return dispatch({
+          type: 'setError',
+          value: _(msg`Please choose your password.`),
+        })
+      }
+      if (!state.handle) {
+        dispatch({type: 'setStep', value: SignupStep.HANDLE})
+        return dispatch({
+          type: 'setError',
+          value: _(msg`Please choose your handle.`),
+        })
+      }
+      if (
+        state.serviceDescription?.phoneVerificationRequired &&
+        !verificationCode
+      ) {
+        dispatch({type: 'setStep', value: SignupStep.CAPTCHA})
+        return dispatch({
+          type: 'setError',
+          value: _(msg`Please complete the verification captcha.`),
+        })
+      }
+      dispatch({type: 'setError', value: ''})
+      dispatch({type: 'setIsLoading', value: true})
+
+      try {
+        onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
+        await createAccount({
+          service: state.serviceUrl,
+          email: state.email,
+          handle: createFullHandle(state.handle, state.userDomain),
+          password: state.password,
+          inviteCode: state.inviteCode.trim(),
+          verificationCode: verificationCode,
+        })
+        setBirthDate({birthDate: state.dateOfBirth})
+        if (IS_PROD_SERVICE(state.serviceUrl)) {
+          setSavedFeeds(DEFAULT_PROD_FEEDS)
+        }
+      } catch (e: any) {
+        onboardingDispatch({type: 'skip'}) // undo starting the onboard
+        let errMsg = e.toString()
+        if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
+          dispatch({
+            type: 'setError',
+            value: _(
+              msg`Invite code not accepted. Check that you input it correctly and try again.`,
+            ),
+          })
+          dispatch({type: 'setStep', value: SignupStep.INFO})
+          return
+        }
+
+        if ([400, 429].includes(e.status)) {
+          logger.warn('Failed to create account', {message: e})
+        } else {
+          logger.error(`Failed to create account (${e.status} status)`, {
+            message: e,
+          })
+        }
+
+        const error = cleanError(errMsg)
+        const isHandleError = error.toLowerCase().includes('handle')
+
+        dispatch({type: 'setIsLoading', value: false})
+        dispatch({type: 'setError', value: cleanError(errMsg)})
+        dispatch({type: 'setStep', value: isHandleError ? 2 : 1})
+      } finally {
+        dispatch({type: 'setIsLoading', value: false})
+      }
+    },
+    [
+      state.email,
+      state.password,
+      state.handle,
+      state.serviceDescription?.phoneVerificationRequired,
+      state.serviceUrl,
+      state.userDomain,
+      state.inviteCode,
+      state.dateOfBirth,
+      dispatch,
+      _,
+      onboardingDispatch,
+      createAccount,
+      setBirthDate,
+      setSavedFeeds,
+    ],
+  )
+}
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 603abbab2..b22bbb7fe 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -5,16 +5,16 @@ import {useLingui} from '@lingui/react'
 import {Trans, msg} from '@lingui/macro'
 import {useNavigation} from '@react-navigation/native'
 
-import {isIOS, isNative} from 'platform/detection'
-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'
-import {useAnalytics} from 'lib/analytics/analytics'
+import {isIOS, isNative} from '#/platform/detection'
+import {Login} from '#/screens/Login'
+import {Signup} from '#/screens/Signup'
+import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
+import {s} from '#/lib/styles'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useAnalytics} from '#/lib/analytics/analytics'
 import {SplashScreen} from './SplashScreen'
 import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {
   useLoggedOutView,
   useLoggedOutViewControls,
@@ -148,7 +148,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
           />
         ) : undefined}
         {screenState === ScreenState.S_CreateAccount ? (
-          <CreateAccount
+          <Signup
             onPressBack={() =>
               setScreenState(ScreenState.S_LoginOrCreateAccount)
             }
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
deleted file mode 100644
index d193802fe..000000000
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  ScrollView,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useCreateAccount, useSubmitCreateAccount} from './state'
-import {useServiceQuery} from '#/state/queries/service'
-import {FEEDBACK_FORM_URL, HITSLOP_10} from '#/lib/constants'
-
-import {Step1} from './Step1'
-import {Step2} from './Step2'
-import {Step3} from './Step3'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {TextLink} from '../../util/Link'
-import {getAgent} from 'state/session'
-import {createFullHandle, validateHandle} from 'lib/strings/handles'
-
-export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
-  const {screen} = useAnalytics()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const [uiState, uiDispatch] = useCreateAccount()
-  const {isTabletOrDesktop} = useWebMediaQueries()
-  const submit = useSubmitCreateAccount(uiState, uiDispatch)
-
-  React.useEffect(() => {
-    screen('CreateAccount')
-  }, [screen])
-
-  // fetch service info
-  // =
-
-  const {
-    data: serviceInfo,
-    isFetching: serviceInfoIsFetching,
-    error: serviceInfoError,
-    refetch: refetchServiceInfo,
-  } = useServiceQuery(uiState.serviceUrl)
-
-  React.useEffect(() => {
-    if (serviceInfo) {
-      uiDispatch({type: 'set-service-description', value: serviceInfo})
-      uiDispatch({type: 'set-error', value: ''})
-    } else if (serviceInfoError) {
-      uiDispatch({
-        type: 'set-error',
-        value: _(
-          msg`Unable to contact your service. Please check your Internet connection.`,
-        ),
-      })
-    }
-  }, [_, uiDispatch, serviceInfo, serviceInfoError])
-
-  // event handlers
-  // =
-
-  const onPressBackInner = React.useCallback(() => {
-    if (uiState.canBack) {
-      uiDispatch({type: 'back'})
-    } else {
-      onPressBack()
-    }
-  }, [uiState, uiDispatch, onPressBack])
-
-  const onPressNext = React.useCallback(async () => {
-    if (!uiState.canNext) {
-      return
-    }
-
-    if (uiState.step === 2) {
-      if (!validateHandle(uiState.handle, uiState.userDomain).overall) {
-        return
-      }
-
-      uiDispatch({type: 'set-processing', value: true})
-      try {
-        const res = await getAgent().resolveHandle({
-          handle: createFullHandle(uiState.handle, uiState.userDomain),
-        })
-
-        if (res.data.did) {
-          uiDispatch({
-            type: 'set-error',
-            value: _(msg`That handle is already taken.`),
-          })
-          return
-        }
-      } catch (e) {
-        // Don't need to handle
-      } finally {
-        uiDispatch({type: 'set-processing', value: false})
-      }
-
-      if (!uiState.isCaptchaRequired) {
-        try {
-          await submit()
-        } catch {
-          // dont need to handle here
-        }
-        // We don't need to go to the next page if there wasn't a captcha required
-        return
-      }
-    }
-
-    uiDispatch({type: 'next'})
-  }, [
-    uiState.canNext,
-    uiState.step,
-    uiState.isCaptchaRequired,
-    uiState.handle,
-    uiState.userDomain,
-    uiDispatch,
-    _,
-    submit,
-  ])
-
-  // rendering
-  // =
-
-  return (
-    <LoggedOutLayout
-      leadin=""
-      title={_(msg`Create Account`)}
-      description={_(msg`We're so excited to have you join us!`)}>
-      <ScrollView
-        testID="createAccount"
-        style={pal.view}
-        keyboardShouldPersistTaps="handled"
-        keyboardDismissMode="on-drag">
-        <View style={styles.stepContainer}>
-          {uiState.step === 1 && (
-            <Step1 uiState={uiState} uiDispatch={uiDispatch} />
-          )}
-          {uiState.step === 2 && (
-            <Step2 uiState={uiState} uiDispatch={uiDispatch} />
-          )}
-          {uiState.step === 3 && (
-            <Step3 uiState={uiState} uiDispatch={uiDispatch} />
-          )}
-        </View>
-        <View style={[s.flexRow, s.pl20, s.pr20]}>
-          <TouchableOpacity
-            onPress={onPressBackInner}
-            testID="backBtn"
-            accessibilityRole="button"
-            hitSlop={HITSLOP_10}>
-            <Text type="xl" style={pal.link}>
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {uiState.canNext ? (
-            <TouchableOpacity
-              testID="nextBtn"
-              onPress={onPressNext}
-              accessibilityRole="button"
-              hitSlop={HITSLOP_10}>
-              {uiState.isProcessing ? (
-                <ActivityIndicator />
-              ) : (
-                <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                  <Trans>Next</Trans>
-                </Text>
-              )}
-            </TouchableOpacity>
-          ) : serviceInfoError ? (
-            <TouchableOpacity
-              testID="retryConnectBtn"
-              onPress={() => refetchServiceInfo()}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Retry`)}
-              accessibilityHint=""
-              accessibilityLiveRegion="polite"
-              hitSlop={HITSLOP_10}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                <Trans>Retry</Trans>
-              </Text>
-            </TouchableOpacity>
-          ) : serviceInfoIsFetching ? (
-            <>
-              <ActivityIndicator color="#fff" />
-              <Text type="xl" style={[pal.text, s.pr5]}>
-                <Trans>Connecting...</Trans>
-              </Text>
-            </>
-          ) : undefined}
-        </View>
-
-        <View style={styles.stepContainer}>
-          <View
-            style={[
-              s.flexRow,
-              s.alignCenter,
-              pal.viewLight,
-              {borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12},
-            ]}>
-            <Text type="md" style={pal.textLight}>
-              <Trans>Having trouble?</Trans>{' '}
-            </Text>
-            <TextLink
-              type="md"
-              style={pal.link}
-              text={_(msg`Contact support`)}
-              href={FEEDBACK_FORM_URL({email: uiState.email})}
-            />
-          </View>
-        </View>
-
-        <View style={{height: isTabletOrDesktop ? 50 : 400}} />
-      </ScrollView>
-    </LoggedOutLayout>
-  )
-}
-
-const styles = StyleSheet.create({
-  stepContainer: {
-    paddingHorizontal: 20,
-    paddingVertical: 20,
-  },
-})
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
deleted file mode 100644
index 803e2ad32..000000000
--- a/src/view/com/auth/create/Policies.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import {TextLink} from '../../util/Link'
-import {Text} from '../../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const Policies = ({
-  serviceDescription,
-  needsGuardian,
-}: {
-  serviceDescription: ServiceDescription
-  needsGuardian: boolean
-}) => {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  if (!serviceDescription) {
-    return <View />
-  }
-  const tos = validWebLink(serviceDescription.links?.termsOfService)
-  const pp = validWebLink(serviceDescription.links?.privacyPolicy)
-  if (!tos && !pp) {
-    return (
-      <View style={[styles.policies, {flexDirection: 'row'}]}>
-        <View
-          style={[
-            styles.errorIcon,
-            {borderColor: pal.colors.text, marginTop: 1},
-          ]}>
-          <FontAwesomeIcon
-            icon="exclamation"
-            style={pal.textLight as FontAwesomeIconStyle}
-            size={10}
-          />
-        </View>
-        <Text style={[pal.textLight, s.pl5, s.flex1]}>
-          <Trans>
-            This service has not provided terms of service or a privacy policy.
-          </Trans>
-        </Text>
-      </View>
-    )
-  }
-  const els = []
-  if (tos) {
-    els.push(
-      <TextLink
-        key="tos"
-        href={tos}
-        text={_(msg`Terms of Service`)}
-        style={[pal.link, s.underline]}
-      />,
-    )
-  }
-  if (pp) {
-    els.push(
-      <TextLink
-        key="pp"
-        href={pp}
-        text={_(msg`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}>
-        <Trans>By creating an account you agree to the {els}.</Trans>
-      </Text>
-      {needsGuardian && (
-        <Text style={[pal.textLight, s.bold]}>
-          <Trans>
-            If you are not yet an adult according to the laws of your country,
-            your parent or legal guardian must read these Terms on your behalf.
-          </Trans>
-        </Text>
-      )}
-    </View>
-  )
-}
-
-function validWebLink(url?: string): string | undefined {
-  return url && (url.startsWith('http://') || url.startsWith('https://'))
-    ? url
-    : undefined
-}
-
-const styles = StyleSheet.create({
-  policies: {
-    flexDirection: 'column',
-    gap: 8,
-  },
-  errorIcon: {
-    borderWidth: 1,
-    borderColor: colors.white,
-    borderRadius: 30,
-    width: 16,
-    height: 16,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-})
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
deleted file mode 100644
index 1f6852f8c..000000000
--- a/src/view/com/auth/create/Step1.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  Keyboard,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {CreateAccountState, CreateAccountDispatch, is18} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {DateInput} from 'view/com/util/forms/DateInput'
-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'
-import {isWeb} from 'platform/detection'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {logger} from '#/logger'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-import {toNiceDomain} from '#/lib/strings/url-helpers'
-
-function sanitizeDate(date: Date): Date {
-  if (!date || date.toString() === 'Invalid Date') {
-    logger.error(`Create account: handled invalid date for birthDate`, {
-      hasDate: !!date,
-    })
-    return new Date()
-  }
-  return date
-}
-
-export function Step1({
-  uiState,
-  uiDispatch,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const serverInputControl = useDialogControl()
-
-  const onPressSelectService = React.useCallback(() => {
-    serverInputControl.open()
-    Keyboard.dismiss()
-  }, [serverInputControl])
-
-  const birthDate = React.useMemo(() => {
-    return sanitizeDate(uiState.birthDate)
-  }, [uiState.birthDate])
-
-  return (
-    <View>
-      <ServerInputDialog
-        control={serverInputControl}
-        onSelect={url => uiDispatch({type: 'set-service-url', value: url})}
-      />
-      <StepHeader uiState={uiState} title={_(msg`Your account`)} />
-
-      {uiState.error ? (
-        <ErrorMessage message={uiState.error} style={styles.error} />
-      ) : undefined}
-
-      <View style={s.pb20}>
-        <Text type="md-medium" style={[pal.text, s.mb2]}>
-          <Trans>Hosting provider</Trans>
-        </Text>
-        <View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}>
-          <View
-            style={[
-              pal.borderDark,
-              {flexDirection: 'row', alignItems: 'center'},
-            ]}>
-            <FontAwesomeIcon
-              icon="globe"
-              style={[pal.textLight, {marginLeft: 14}]}
-            />
-            <TouchableOpacity
-              testID="selectServiceButton"
-              style={{
-                flexDirection: 'row',
-                flex: 1,
-                alignItems: 'center',
-              }}
-              onPress={onPressSelectService}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Select service`)}
-              accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
-              <Text
-                type="xl"
-                style={[
-                  pal.text,
-                  {
-                    flex: 1,
-                    paddingVertical: 10,
-                    paddingRight: 12,
-                    paddingLeft: 10,
-                  },
-                ]}>
-                {toNiceDomain(uiState.serviceUrl)}
-              </Text>
-              <View
-                style={[
-                  pal.btn,
-                  {
-                    flexDirection: 'row',
-                    alignItems: 'center',
-                    borderRadius: 6,
-                    paddingVertical: 6,
-                    paddingHorizontal: 8,
-                    marginHorizontal: 6,
-                  },
-                ]}>
-                <FontAwesomeIcon
-                  icon="pen"
-                  size={12}
-                  style={pal.textLight as FontAwesomeIconStyle}
-                />
-              </View>
-            </TouchableOpacity>
-          </View>
-        </View>
-      </View>
-
-      {!uiState.serviceDescription ? (
-        <ActivityIndicator />
-      ) : (
-        <>
-          {uiState.isInviteCodeRequired && (
-            <View style={s.pb20}>
-              <Text type="md-medium" style={[pal.text, s.mb2]}>
-                <Trans>Invite code</Trans>
-              </Text>
-              <TextInput
-                testID="inviteCodeInput"
-                icon="ticket"
-                placeholder={_(msg`Required for this provider`)}
-                value={uiState.inviteCode}
-                editable
-                onChange={value => uiDispatch({type: 'set-invite-code', value})}
-                accessibilityLabel={_(msg`Invite code`)}
-                accessibilityHint={_(msg`Input invite code to proceed`)}
-                autoCapitalize="none"
-                autoComplete="off"
-                autoCorrect={false}
-                autoFocus={true}
-              />
-            </View>
-          )}
-
-          {!uiState.isInviteCodeRequired || uiState.inviteCode ? (
-            <>
-              <View style={s.pb20}>
-                <Text
-                  type="md-medium"
-                  style={[pal.text, s.mb2]}
-                  nativeID="email">
-                  <Trans>Email address</Trans>
-                </Text>
-                <TextInput
-                  testID="emailInput"
-                  icon="envelope"
-                  placeholder={_(msg`Enter your email address`)}
-                  value={uiState.email}
-                  editable
-                  onChange={value => uiDispatch({type: 'set-email', value})}
-                  accessibilityLabel={_(msg`Email`)}
-                  accessibilityHint={_(msg`Input email for Bluesky account`)}
-                  accessibilityLabelledBy="email"
-                  autoCapitalize="none"
-                  autoComplete="email"
-                  autoCorrect={false}
-                  autoFocus={!uiState.isInviteCodeRequired}
-                />
-              </View>
-
-              <View style={s.pb20}>
-                <Text
-                  type="md-medium"
-                  style={[pal.text, s.mb2]}
-                  nativeID="password">
-                  <Trans>Password</Trans>
-                </Text>
-                <TextInput
-                  testID="passwordInput"
-                  icon="lock"
-                  placeholder={_(msg`Choose your password`)}
-                  value={uiState.password}
-                  editable
-                  secureTextEntry
-                  onChange={value => uiDispatch({type: 'set-password', value})}
-                  accessibilityLabel={_(msg`Password`)}
-                  accessibilityHint={_(msg`Set password`)}
-                  accessibilityLabelledBy="password"
-                  autoCapitalize="none"
-                  autoComplete="new-password"
-                  autoCorrect={false}
-                />
-              </View>
-
-              <View style={s.pb20}>
-                <Text
-                  type="md-medium"
-                  style={[pal.text, s.mb2]}
-                  nativeID="birthDate">
-                  <Trans>Your birth date</Trans>
-                </Text>
-                <DateInput
-                  handleAsUTC
-                  testID="birthdayInput"
-                  value={birthDate}
-                  onChange={value =>
-                    uiDispatch({type: 'set-birth-date', value})
-                  }
-                  buttonType="default-light"
-                  buttonStyle={[pal.border, styles.dateInputButton]}
-                  buttonLabelType="lg"
-                  accessibilityLabel={_(msg`Birthday`)}
-                  accessibilityHint={_(msg`Enter your birth date`)}
-                  accessibilityLabelledBy="birthDate"
-                />
-              </View>
-
-              {uiState.serviceDescription && (
-                <Policies
-                  serviceDescription={uiState.serviceDescription}
-                  needsGuardian={!is18(uiState)}
-                />
-              )}
-            </>
-          ) : undefined}
-        </>
-      )}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  error: {
-    borderRadius: 6,
-    marginBottom: 10,
-  },
-  dateInputButton: {
-    borderWidth: 1,
-    borderRadius: 6,
-    paddingVertical: 14,
-  },
-  // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
-  touchable: {
-    ...(isWeb && {cursor: 'pointer'}),
-  },
-})
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
deleted file mode 100644
index 5c262977f..000000000
--- a/src/view/com/auth/create/Step2.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {CreateAccountState, CreateAccountDispatch} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {StepHeader} from './StepHeader'
-import {s} from 'lib/styles'
-import {TextInput} from '../util/TextInput'
-import {
-  createFullHandle,
-  IsValidHandle,
-  validateHandle,
-} from 'lib/strings/handles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {atoms as a, useTheme} from '#/alf'
-import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
-import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
-import {useFocusEffect} from '@react-navigation/native'
-
-/** STEP 3: Your user handle
- * @field User handle
- */
-export function Step2({
-  uiState,
-  uiDispatch,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const t = useTheme()
-
-  const [validCheck, setValidCheck] = React.useState<IsValidHandle>({
-    handleChars: false,
-    frontLength: false,
-    totalLength: true,
-    overall: false,
-  })
-
-  useFocusEffect(
-    React.useCallback(() => {
-      setValidCheck(validateHandle(uiState.handle, uiState.userDomain))
-
-      // Disabling this, because we only want to run this when we focus the screen
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []),
-  )
-
-  const onHandleChange = React.useCallback(
-    (value: string) => {
-      if (uiState.error) {
-        uiDispatch({type: 'set-error', value: ''})
-      }
-
-      setValidCheck(validateHandle(value, uiState.userDomain))
-      uiDispatch({type: 'set-handle', value})
-    },
-    [uiDispatch, uiState.error, uiState.userDomain],
-  )
-
-  return (
-    <View>
-      <StepHeader uiState={uiState} title={_(msg`Your user handle`)} />
-      <View style={s.pb10}>
-        <View style={s.mb20}>
-          <TextInput
-            testID="handleInput"
-            icon="at"
-            placeholder="e.g. alice"
-            value={uiState.handle}
-            editable
-            autoFocus
-            autoComplete="off"
-            autoCorrect={false}
-            onChange={onHandleChange}
-            // TODO: Add explicit text label
-            accessibilityLabel={_(msg`User handle`)}
-            accessibilityHint={_(msg`Input your user handle`)}
-          />
-          <Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
-            <Trans>Your full handle will be</Trans>{' '}
-            <Text type="lg-bold" style={pal.text}>
-              @{createFullHandle(uiState.handle, uiState.userDomain)}
-            </Text>
-          </Text>
-        </View>
-        <View
-          style={[
-            a.w_full,
-            a.rounded_sm,
-            a.border,
-            a.p_md,
-            a.gap_sm,
-            t.atoms.border_contrast_low,
-          ]}>
-          {uiState.error ? (
-            <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
-              <IsValidIcon valid={false} />
-              <Text style={[t.atoms.text, a.text_md, a.flex]}>
-                {uiState.error}
-              </Text>
-            </View>
-          ) : undefined}
-          <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
-            <IsValidIcon valid={validCheck.handleChars} />
-            <Text style={[t.atoms.text, a.text_md, a.flex]}>
-              <Trans>May only contain letters and numbers</Trans>
-            </Text>
-          </View>
-          <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
-            <IsValidIcon
-              valid={validCheck.frontLength && validCheck.totalLength}
-            />
-            {!validCheck.totalLength ? (
-              <Text style={[t.atoms.text]}>
-                <Trans>May not be longer than 253 characters</Trans>
-              </Text>
-            ) : (
-              <Text style={[t.atoms.text, a.text_md]}>
-                <Trans>Must be at least 3 characters</Trans>
-              </Text>
-            )}
-          </View>
-        </View>
-      </View>
-    </View>
-  )
-}
-
-function IsValidIcon({valid}: {valid: boolean}) {
-  const t = useTheme()
-
-  if (!valid) {
-    return <Times size="md" style={{color: t.palette.negative_500}} />
-  }
-
-  return <Check size="md" style={{color: t.palette.positive_700}} />
-}
diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx
deleted file mode 100644
index 53fdfdde8..000000000
--- a/src/view/com/auth/create/Step3.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from 'react'
-import {ActivityIndicator, StyleSheet, View} from 'react-native'
-import {
-  CreateAccountState,
-  CreateAccountDispatch,
-  useSubmitCreateAccount,
-} from './state'
-import {StepHeader} from './StepHeader'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {isWeb} from 'platform/detection'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {nanoid} from 'nanoid/non-secure'
-import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView'
-import {useTheme} from 'lib/ThemeContext'
-import {createFullHandle} from 'lib/strings/handles'
-
-const CAPTCHA_PATH = '/gate/signup'
-
-export function Step3({
-  uiState,
-  uiDispatch,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-}) {
-  const {_} = useLingui()
-  const theme = useTheme()
-  const submit = useSubmitCreateAccount(uiState, uiDispatch)
-
-  const [completed, setCompleted] = React.useState(false)
-
-  const stateParam = React.useMemo(() => nanoid(15), [])
-  const url = React.useMemo(() => {
-    const newUrl = new URL(uiState.serviceUrl)
-    newUrl.pathname = CAPTCHA_PATH
-    newUrl.searchParams.set(
-      'handle',
-      createFullHandle(uiState.handle, uiState.userDomain),
-    )
-    newUrl.searchParams.set('state', stateParam)
-    newUrl.searchParams.set('colorScheme', theme.colorScheme)
-
-    console.log(newUrl)
-
-    return newUrl.href
-  }, [
-    uiState.serviceUrl,
-    uiState.handle,
-    uiState.userDomain,
-    stateParam,
-    theme.colorScheme,
-  ])
-
-  const onSuccess = React.useCallback(
-    (code: string) => {
-      setCompleted(true)
-      submit(code)
-    },
-    [submit],
-  )
-
-  const onError = React.useCallback(() => {
-    uiDispatch({
-      type: 'set-error',
-      value: _(msg`Error receiving captcha response.`),
-    })
-  }, [_, uiDispatch])
-
-  return (
-    <View>
-      <StepHeader uiState={uiState} title={_(msg`Complete the challenge`)} />
-      <View style={[styles.container, completed && styles.center]}>
-        {!completed ? (
-          <CaptchaWebView
-            url={url}
-            stateParam={stateParam}
-            uiState={uiState}
-            onSuccess={onSuccess}
-            onError={onError}
-          />
-        ) : (
-          <ActivityIndicator size="large" />
-        )}
-      </View>
-
-      {uiState.error ? (
-        <ErrorMessage message={uiState.error} style={styles.error} />
-      ) : undefined}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  error: {
-    borderRadius: 6,
-    marginTop: 10,
-  },
-  // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
-  touchable: {
-    ...(isWeb && {cursor: 'pointer'}),
-  },
-  container: {
-    minHeight: 500,
-    width: '100%',
-    paddingBottom: 20,
-    overflow: 'hidden',
-  },
-  center: {
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-})
diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx
deleted file mode 100644
index a98b392d8..000000000
--- a/src/view/com/auth/create/StepHeader.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {Text} from 'view/com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans} from '@lingui/macro'
-import {CreateAccountState} from './state'
-
-export function StepHeader({
-  uiState,
-  title,
-  children,
-}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) {
-  const pal = usePalette('default')
-  const numSteps = 3
-  return (
-    <View style={styles.container}>
-      <View>
-        <Text type="lg" style={[pal.textLight]}>
-          {uiState.step === 3 ? (
-            <Trans>Last step!</Trans>
-          ) : (
-            <Trans>
-              Step {uiState.step} of {numSteps}
-            </Trans>
-          )}
-        </Text>
-
-        <Text style={[pal.text]} type="title-xl">
-          {title}
-        </Text>
-      </View>
-      {children}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    marginBottom: 20,
-  },
-})
diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts
deleted file mode 100644
index 840084dcb..000000000
--- a/src/view/com/auth/create/state.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import {useCallback, useReducer} from 'react'
-import {
-  ComAtprotoServerDescribeServer,
-  ComAtprotoServerCreateAccount,
-} from '@atproto/api'
-import {I18nContext, useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
-import * as EmailValidator from 'email-validator'
-import {getAge} from 'lib/strings/time'
-import {logger} from '#/logger'
-import {createFullHandle, validateHandle} from '#/lib/strings/handles'
-import {cleanError} from '#/lib/strings/errors'
-import {useOnboardingDispatch} from '#/state/shell/onboarding'
-import {useSessionApi} from '#/state/session'
-import {DEFAULT_SERVICE, IS_TEST_USER} from '#/lib/constants'
-import {
-  DEFAULT_PROD_FEEDS,
-  usePreferencesSetBirthDateMutation,
-  useSetSaveFeedsMutation,
-} from 'state/queries/preferences'
-
-export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
-
-export type CreateAccountAction =
-  | {type: 'set-step'; value: number}
-  | {type: 'set-error'; value: string | undefined}
-  | {type: 'set-processing'; value: boolean}
-  | {type: 'set-service-url'; value: string}
-  | {type: 'set-service-description'; value: ServiceDescription | undefined}
-  | {type: 'set-user-domain'; value: string}
-  | {type: 'set-invite-code'; value: string}
-  | {type: 'set-email'; value: string}
-  | {type: 'set-password'; value: string}
-  | {type: 'set-handle'; value: string}
-  | {type: 'set-birth-date'; value: Date}
-  | {type: 'next'}
-  | {type: 'back'}
-
-export interface CreateAccountState {
-  // state
-  step: number
-  error: string | undefined
-  isProcessing: boolean
-  serviceUrl: string
-  serviceDescription: ServiceDescription | undefined
-  userDomain: string
-  inviteCode: string
-  email: string
-  password: string
-  handle: string
-  birthDate: Date
-
-  // computed
-  canBack: boolean
-  canNext: boolean
-  isInviteCodeRequired: boolean
-  isCaptchaRequired: boolean
-}
-
-export type CreateAccountDispatch = (action: CreateAccountAction) => void
-
-export function useCreateAccount() {
-  const {_} = useLingui()
-
-  return useReducer(createReducer({_}), {
-    step: 1,
-    error: undefined,
-    isProcessing: false,
-    serviceUrl: DEFAULT_SERVICE,
-    serviceDescription: undefined,
-    userDomain: '',
-    inviteCode: '',
-    email: '',
-    password: '',
-    handle: '',
-    birthDate: DEFAULT_DATE,
-
-    canBack: false,
-    canNext: false,
-    isInviteCodeRequired: false,
-    isCaptchaRequired: false,
-  })
-}
-
-export function useSubmitCreateAccount(
-  uiState: CreateAccountState,
-  uiDispatch: CreateAccountDispatch,
-) {
-  const {_} = useLingui()
-  const {createAccount} = useSessionApi()
-  const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
-  const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
-  const onboardingDispatch = useOnboardingDispatch()
-
-  return useCallback(
-    async (verificationCode?: string) => {
-      if (!uiState.email) {
-        uiDispatch({type: 'set-step', value: 1})
-        console.log('no email?')
-        return uiDispatch({
-          type: 'set-error',
-          value: _(msg`Please enter your email.`),
-        })
-      }
-      if (!EmailValidator.validate(uiState.email)) {
-        uiDispatch({type: 'set-step', value: 1})
-        return uiDispatch({
-          type: 'set-error',
-          value: _(msg`Your email appears to be invalid.`),
-        })
-      }
-      if (!uiState.password) {
-        uiDispatch({type: 'set-step', value: 1})
-        return uiDispatch({
-          type: 'set-error',
-          value: _(msg`Please choose your password.`),
-        })
-      }
-      if (!uiState.handle) {
-        uiDispatch({type: 'set-step', value: 2})
-        return uiDispatch({
-          type: 'set-error',
-          value: _(msg`Please choose your handle.`),
-        })
-      }
-      if (uiState.isCaptchaRequired && !verificationCode) {
-        uiDispatch({type: 'set-step', value: 3})
-        return uiDispatch({
-          type: 'set-error',
-          value: _(msg`Please complete the verification captcha.`),
-        })
-      }
-      uiDispatch({type: 'set-error', value: ''})
-      uiDispatch({type: 'set-processing', value: true})
-
-      try {
-        onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
-        await createAccount({
-          service: uiState.serviceUrl,
-          email: uiState.email,
-          handle: createFullHandle(uiState.handle, uiState.userDomain),
-          password: uiState.password,
-          inviteCode: uiState.inviteCode.trim(),
-          verificationCode: uiState.isCaptchaRequired
-            ? verificationCode
-            : undefined,
-        })
-        setBirthDate({birthDate: uiState.birthDate})
-        if (!IS_TEST_USER(uiState.handle)) {
-          setSavedFeeds(DEFAULT_PROD_FEEDS)
-        }
-      } catch (e: any) {
-        onboardingDispatch({type: 'skip'}) // undo starting the onboard
-        let errMsg = e.toString()
-        if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
-          errMsg = _(
-            msg`Invite code not accepted. Check that you input it correctly and try again.`,
-          )
-          uiDispatch({type: 'set-step', value: 1})
-        }
-
-        if ([400, 429].includes(e.status)) {
-          logger.warn('Failed to create account', {message: e})
-        } else {
-          logger.error(`Failed to create account (${e.status} status)`, {
-            message: e,
-          })
-        }
-
-        const error = cleanError(errMsg)
-        const isHandleError = error.toLowerCase().includes('handle')
-
-        uiDispatch({type: 'set-processing', value: false})
-        uiDispatch({type: 'set-error', value: cleanError(errMsg)})
-        uiDispatch({type: 'set-step', value: isHandleError ? 2 : 1})
-      }
-    },
-    [
-      uiState.email,
-      uiState.password,
-      uiState.handle,
-      uiState.isCaptchaRequired,
-      uiState.serviceUrl,
-      uiState.userDomain,
-      uiState.inviteCode,
-      uiState.birthDate,
-      uiDispatch,
-      _,
-      onboardingDispatch,
-      createAccount,
-      setBirthDate,
-      setSavedFeeds,
-    ],
-  )
-}
-
-export function is13(state: CreateAccountState) {
-  return getAge(state.birthDate) >= 13
-}
-
-export function is18(state: CreateAccountState) {
-  return getAge(state.birthDate) >= 18
-}
-
-function createReducer({_}: {_: I18nContext['_']}) {
-  return function reducer(
-    state: CreateAccountState,
-    action: CreateAccountAction,
-  ): CreateAccountState {
-    switch (action.type) {
-      case 'set-step': {
-        return compute({...state, step: action.value})
-      }
-      case 'set-error': {
-        return compute({...state, error: action.value})
-      }
-      case 'set-processing': {
-        return compute({...state, isProcessing: action.value})
-      }
-      case 'set-service-url': {
-        return compute({
-          ...state,
-          serviceUrl: action.value,
-          serviceDescription:
-            state.serviceUrl !== action.value
-              ? undefined
-              : state.serviceDescription,
-        })
-      }
-      case 'set-service-description': {
-        return compute({
-          ...state,
-          serviceDescription: action.value,
-          userDomain: action.value?.availableUserDomains[0] || '',
-        })
-      }
-      case 'set-user-domain': {
-        return compute({...state, userDomain: action.value})
-      }
-      case 'set-invite-code': {
-        return compute({...state, inviteCode: action.value})
-      }
-      case 'set-email': {
-        return compute({...state, email: action.value})
-      }
-      case 'set-password': {
-        return compute({...state, password: action.value})
-      }
-      case 'set-handle': {
-        return compute({...state, handle: action.value})
-      }
-      case 'set-birth-date': {
-        return compute({...state, birthDate: action.value})
-      }
-      case 'next': {
-        if (state.step === 1) {
-          if (!is13(state)) {
-            return compute({
-              ...state,
-              error: _(
-                msg`Unfortunately, you do not meet the requirements to create an account.`,
-              ),
-            })
-          }
-        }
-        return compute({...state, error: '', step: state.step + 1})
-      }
-      case 'back': {
-        return compute({...state, error: '', step: state.step - 1})
-      }
-    }
-  }
-}
-
-function compute(state: CreateAccountState): CreateAccountState {
-  let canNext = true
-  if (state.step === 1) {
-    canNext =
-      !!state.serviceDescription &&
-      (!state.isInviteCodeRequired || !!state.inviteCode) &&
-      !!state.email &&
-      !!state.password
-  } else if (state.step === 2) {
-    canNext =
-      !!state.handle && validateHandle(state.handle, state.userDomain).overall
-  } else if (state.step === 3) {
-    // Step 3 will automatically redirect as soon as the captcha completes
-    canNext = false
-  }
-  return {
-    ...state,
-    canBack: state.step > 1,
-    canNext,
-    isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired,
-    isCaptchaRequired: !!state.serviceDescription?.phoneVerificationRequired,
-  }
-}
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx
deleted file mode 100644
index e754c8483..000000000
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React from 'react'
-import {ScrollView, TouchableOpacity, View} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {UserAvatar} from '../../util/UserAvatar'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {styles} from './styles'
-import {useSession, useSessionApi, SessionAccount} from '#/state/session'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import * as Toast from '#/view/com/util/Toast'
-import {logEvent} from '#/lib/statsig/statsig'
-
-function AccountItem({
-  account,
-  onSelect,
-  isCurrentAccount,
-}: {
-  account: SessionAccount
-  onSelect: (account: SessionAccount) => void
-  isCurrentAccount: boolean
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {data: profile} = useProfileQuery({did: account.did})
-
-  const onPress = React.useCallback(() => {
-    onSelect(account)
-  }, [account, onSelect])
-
-  return (
-    <TouchableOpacity
-      testID={`chooseAccountBtn-${account.handle}`}
-      key={account.did}
-      style={[pal.view, pal.border, styles.account]}
-      onPress={onPress}
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
-      accessibilityHint={_(msg`Double tap to sign in`)}>
-      <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-        <View style={s.p10}>
-          <UserAvatar
-            avatar={profile?.avatar}
-            size={30}
-            type={profile?.associated?.labeler ? 'labeler' : 'user'}
-          />
-        </View>
-        <Text style={styles.accountText}>
-          <Text type="lg-bold" style={pal.text}>
-            {profile?.displayName || account.handle}{' '}
-          </Text>
-          <Text type="lg" style={[pal.textLight]}>
-            {account.handle}
-          </Text>
-        </Text>
-        {isCurrentAccount ? (
-          <FontAwesomeIcon
-            icon="check"
-            size={16}
-            style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]}
-          />
-        ) : (
-          <FontAwesomeIcon
-            icon="angle-right"
-            size={16}
-            style={[pal.text, s.mr10]}
-          />
-        )}
-      </View>
-    </TouchableOpacity>
-  )
-}
-export const ChooseAccountForm = ({
-  onSelectAccount,
-  onPressBack,
-}: {
-  onSelectAccount: (account?: SessionAccount) => void
-  onPressBack: () => void
-}) => {
-  const {track, screen} = useAnalytics()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {accounts, currentAccount} = useSession()
-  const {initSession} = useSessionApi()
-  const {setShowLoggedOut} = useLoggedOutViewControls()
-
-  React.useEffect(() => {
-    screen('Choose Account')
-  }, [screen])
-
-  const onSelect = React.useCallback(
-    async (account: SessionAccount) => {
-      if (account.accessJwt) {
-        if (account.did === currentAccount?.did) {
-          setShowLoggedOut(false)
-          Toast.show(_(msg`Already signed in as @${account.handle}`))
-        } else {
-          await initSession(account)
-          logEvent('account:loggedIn', {
-            logContext: 'ChooseAccountForm',
-            withPassword: false,
-          })
-          track('Sign In', {resumedSession: true})
-          setTimeout(() => {
-            Toast.show(_(msg`Signed in as @${account.handle}`))
-          }, 100)
-        }
-      } else {
-        onSelectAccount(account)
-      }
-    },
-    [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _],
-  )
-
-  return (
-    <ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
-      <Text
-        type="2xl-medium"
-        style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
-        <Trans>Sign in as...</Trans>
-      </Text>
-      {accounts.map(account => (
-        <AccountItem
-          key={account.did}
-          account={account}
-          onSelect={onSelect}
-          isCurrentAccount={account.did === currentAccount?.did}
-        />
-      ))}
-      <TouchableOpacity
-        testID="chooseNewAccountBtn"
-        style={[pal.view, pal.border, styles.account, styles.accountLast]}
-        onPress={() => onSelectAccount(undefined)}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Login to account that is not listed`)}
-        accessibilityHint="">
-        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <Text style={[styles.accountText, styles.accountTextOther]}>
-            <Text type="lg" style={pal.text}>
-              <Trans>Other account</Trans>
-            </Text>
-          </Text>
-          <FontAwesomeIcon
-            icon="angle-right"
-            size={16}
-            style={[pal.text, s.mr10]}
-          />
-        </View>
-      </TouchableOpacity>
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <TouchableOpacity onPress={onPressBack} accessibilityRole="button">
-          <Text type="xl" style={[pal.link, s.pl5]}>
-            <Trans>Back</Trans>
-          </Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-      </View>
-    </ScrollView>
-  )
-}
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
deleted file mode 100644
index 322da2b8f..000000000
--- a/src/view/com/auth/login/ForgotPasswordForm.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {
-  ActivityIndicator,
-  Keyboard,
-  TextInput,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import * as EmailValidator from 'email-validator'
-import {BskyAgent} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {toNiceDomain} from 'lib/strings/url-helpers'
-import {isNetworkError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {cleanError} from 'lib/strings/errors'
-import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {styles} from './styles'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const ForgotPasswordForm = ({
-  error,
-  serviceUrl,
-  serviceDescription,
-  setError,
-  setServiceUrl,
-  onPressBack,
-  onEmailSent,
-}: {
-  error: string
-  serviceUrl: string
-  serviceDescription: ServiceDescription | undefined
-  setError: (v: string) => void
-  setServiceUrl: (v: string) => void
-  onPressBack: () => void
-  onEmailSent: () => void
-}) => {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [email, setEmail] = useState<string>('')
-  const {screen} = useAnalytics()
-  const {_} = useLingui()
-  const serverInputControl = useDialogControl()
-
-  useEffect(() => {
-    screen('Signin:ForgotPassword')
-  }, [screen])
-
-  const onPressSelectService = React.useCallback(() => {
-    serverInputControl.open()
-    Keyboard.dismiss()
-  }, [serverInputControl])
-
-  const onPressNext = async () => {
-    if (!EmailValidator.validate(email)) {
-      return setError(_(msg`Your email appears to be invalid.`))
-    }
-
-    setError('')
-    setIsProcessing(true)
-
-    try {
-      const agent = new BskyAgent({service: serviceUrl})
-      await agent.com.atproto.server.requestPasswordReset({email})
-      onEmailSent()
-    } catch (e: any) {
-      const errMsg = e.toString()
-      logger.warn('Failed to request password reset', {error: e})
-      setIsProcessing(false)
-      if (isNetworkError(e)) {
-        setError(
-          _(
-            msg`Unable to contact your service. Please check your Internet connection.`,
-          ),
-        )
-      } else {
-        setError(cleanError(errMsg))
-      }
-    }
-  }
-
-  return (
-    <>
-      <View>
-        <ServerInputDialog
-          control={serverInputControl}
-          onSelect={setServiceUrl}
-        />
-        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
-          <Trans>Reset password</Trans>
-        </Text>
-        <Text type="md" style={[pal.text, styles.instructions]}>
-          <Trans>
-            Enter the email you used to create your account. We'll send you a
-            "reset code" so you can set a new password.
-          </Trans>
-        </Text>
-        <View
-          testID="forgotPasswordView"
-          style={[pal.borderDark, pal.view, styles.group]}>
-          <TouchableOpacity
-            testID="forgotPasswordSelectServiceButton"
-            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
-            onPress={onPressSelectService}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Hosting provider`)}
-            accessibilityHint={_(
-              msg`Sets hosting provider for password reset`,
-            )}>
-            <FontAwesomeIcon
-              icon="globe"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <Text style={[pal.text, styles.textInput]} numberOfLines={1}>
-              {toNiceDomain(serviceUrl)}
-            </Text>
-            <View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
-              <FontAwesomeIcon
-                icon="pen"
-                size={12}
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-          </TouchableOpacity>
-          <View style={[pal.borderDark, styles.groupContent]}>
-            <FontAwesomeIcon
-              icon="envelope"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <TextInput
-              testID="forgotPasswordEmail"
-              style={[pal.text, styles.textInput]}
-              placeholder={_(msg`Email address`)}
-              placeholderTextColor={pal.colors.textLight}
-              autoCapitalize="none"
-              autoFocus
-              autoCorrect={false}
-              keyboardAppearance={theme.colorScheme}
-              value={email}
-              onChangeText={setEmail}
-              editable={!isProcessing}
-              accessibilityLabel={_(msg`Email`)}
-              accessibilityHint={_(msg`Sets email for password reset`)}
-            />
-          </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} accessibilityRole="button">
-            <Text type="xl" style={[pal.link, s.pl5]}>
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {!serviceDescription || isProcessing ? (
-            <ActivityIndicator />
-          ) : !email ? (
-            <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
-              <Trans>Next</Trans>
-            </Text>
-          ) : (
-            <TouchableOpacity
-              testID="newPasswordButton"
-              onPress={onPressNext}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Go to next`)}
-              accessibilityHint={_(msg`Navigates to the next screen`)}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                <Trans>Next</Trans>
-              </Text>
-            </TouchableOpacity>
-          )}
-          {!serviceDescription || isProcessing ? (
-            <Text type="xl" style={[pal.textLight, s.pl10]}>
-              <Trans>Processing...</Trans>
-            </Text>
-          ) : undefined}
-        </View>
-        <View
-          style={[
-            s.flexRow,
-            s.alignCenter,
-            s.mt20,
-            s.mb20,
-            pal.border,
-            s.borderBottom1,
-            {alignSelf: 'center', width: '90%'},
-          ]}
-        />
-        <View style={[s.flexRow, s.justifyCenter]}>
-          <TouchableOpacity
-            testID="skipSendEmailButton"
-            onPress={onEmailSent}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Go to next`)}
-            accessibilityHint={_(msg`Navigates to the next screen`)}>
-            <Text type="xl" style={[pal.link, s.pr5]}>
-              <Trans>Already have a code?</Trans>
-            </Text>
-          </TouchableOpacity>
-        </View>
-      </View>
-    </>
-  )
-}
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
deleted file mode 100644
index bc931ac04..000000000
--- a/src/view/com/auth/login/Login.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {KeyboardAvoidingView} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
-import {DEFAULT_SERVICE} from '#/lib/constants'
-import {usePalette} from 'lib/hooks/usePalette'
-import {logger} from '#/logger'
-import {ChooseAccountForm} from './ChooseAccountForm'
-import {LoginForm} from './LoginForm'
-import {ForgotPasswordForm} from './ForgotPasswordForm'
-import {SetNewPasswordForm} from './SetNewPasswordForm'
-import {PasswordUpdatedForm} from './PasswordUpdatedForm'
-import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
-import {useSession, SessionAccount} from '#/state/session'
-import {useServiceQuery} from '#/state/queries/service'
-import {useLoggedOutView} from '#/state/shell/logged-out'
-
-enum Forms {
-  Login,
-  ChooseAccount,
-  ForgotPassword,
-  SetNewPassword,
-  PasswordUpdated,
-}
-
-export const Login = ({onPressBack}: {onPressBack: () => void}) => {
-  const {_} = useLingui()
-  const pal = usePalette('default')
-
-  const {accounts} = useSession()
-  const {track} = useAnalytics()
-  const {requestedAccountSwitchTo} = useLoggedOutView()
-  const requestedAccount = accounts.find(
-    a => a.did === requestedAccountSwitchTo,
-  )
-
-  const [error, setError] = useState<string>('')
-  const [serviceUrl, setServiceUrl] = useState<string>(
-    requestedAccount?.service || DEFAULT_SERVICE,
-  )
-  const [initialHandle, setInitialHandle] = useState<string>(
-    requestedAccount?.handle || '',
-  )
-  const [currentForm, setCurrentForm] = useState<Forms>(
-    requestedAccount
-      ? Forms.Login
-      : accounts.length
-      ? Forms.ChooseAccount
-      : Forms.Login,
-  )
-
-  const {
-    data: serviceDescription,
-    error: serviceError,
-    refetch: refetchService,
-  } = useServiceQuery(serviceUrl)
-
-  const onSelectAccount = (account?: SessionAccount) => {
-    if (account?.service) {
-      setServiceUrl(account.service)
-    }
-    setInitialHandle(account?.handle || '')
-    setCurrentForm(Forms.Login)
-  }
-
-  const gotoForm = (form: Forms) => () => {
-    setError('')
-    setCurrentForm(form)
-  }
-
-  useEffect(() => {
-    if (serviceError) {
-      setError(
-        _(
-          msg`Unable to contact your service. Please check your Internet connection.`,
-        ),
-      )
-      logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
-        error: String(serviceError),
-      })
-    } else {
-      setError('')
-    }
-  }, [serviceError, serviceUrl, _])
-
-  const onPressRetryConnect = () => refetchService()
-  const onPressForgotPassword = () => {
-    track('Signin:PressedForgotPassword')
-    setCurrentForm(Forms.ForgotPassword)
-  }
-
-  return (
-    <KeyboardAvoidingView testID="signIn" behavior="padding" style={pal.view}>
-      {currentForm === Forms.Login ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Sign in`)}
-          description={_(msg`Enter your username and password`)}>
-          <LoginForm
-            error={error}
-            serviceUrl={serviceUrl}
-            serviceDescription={serviceDescription}
-            initialHandle={initialHandle}
-            setError={setError}
-            setServiceUrl={setServiceUrl}
-            onPressBack={onPressBack}
-            onPressForgotPassword={onPressForgotPassword}
-            onPressRetryConnect={onPressRetryConnect}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.ChooseAccount ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Sign in as...`)}
-          description={_(msg`Select from an existing account`)}>
-          <ChooseAccountForm
-            onSelectAccount={onSelectAccount}
-            onPressBack={onPressBack}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.ForgotPassword ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Forgot Password`)}
-          description={_(msg`Let's get your password reset!`)}>
-          <ForgotPasswordForm
-            error={error}
-            serviceUrl={serviceUrl}
-            serviceDescription={serviceDescription}
-            setError={setError}
-            setServiceUrl={setServiceUrl}
-            onPressBack={gotoForm(Forms.Login)}
-            onEmailSent={gotoForm(Forms.SetNewPassword)}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.SetNewPassword ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Forgot Password`)}
-          description={_(msg`Let's get your password reset!`)}>
-          <SetNewPasswordForm
-            error={error}
-            serviceUrl={serviceUrl}
-            setError={setError}
-            onPressBack={gotoForm(Forms.ForgotPassword)}
-            onPasswordSet={gotoForm(Forms.PasswordUpdated)}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.PasswordUpdated ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Password updated`)}
-          description={_(msg`You can now sign in with your new password.`)}>
-          <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} />
-        </LoggedOutLayout>
-      ) : undefined}
-    </KeyboardAvoidingView>
-  )
-}
diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx
deleted file mode 100644
index 92f495575..000000000
--- a/src/view/com/auth/login/LoginForm.tsx
+++ /dev/null
@@ -1,301 +0,0 @@
-import React, {useState, useRef} from 'react'
-import {
-  ActivityIndicator,
-  Keyboard,
-  TextInput,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {createFullHandle} from 'lib/strings/handles'
-import {toNiceDomain} from 'lib/strings/url-helpers'
-import {isNetworkError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {useSessionApi} from '#/state/session'
-import {cleanError} from 'lib/strings/errors'
-import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
-import {styles} from './styles'
-import {useLingui} from '@lingui/react'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const LoginForm = ({
-  error,
-  serviceUrl,
-  serviceDescription,
-  initialHandle,
-  setError,
-  setServiceUrl,
-  onPressRetryConnect,
-  onPressBack,
-  onPressForgotPassword,
-}: {
-  error: string
-  serviceUrl: string
-  serviceDescription: ServiceDescription | undefined
-  initialHandle: string
-  setError: (v: string) => void
-  setServiceUrl: (v: string) => void
-  onPressRetryConnect: () => void
-  onPressBack: () => void
-  onPressForgotPassword: () => void
-}) => {
-  const {track} = useAnalytics()
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [identifier, setIdentifier] = useState<string>(initialHandle)
-  const [password, setPassword] = useState<string>('')
-  const passwordInputRef = useRef<TextInput>(null)
-  const {_} = useLingui()
-  const {login} = useSessionApi()
-  const serverInputControl = useDialogControl()
-
-  const onPressSelectService = () => {
-    serverInputControl.open()
-    Keyboard.dismiss()
-    track('Signin:PressedSelectService')
-  }
-
-  const onPressNext = async () => {
-    Keyboard.dismiss()
-    setError('')
-    setIsProcessing(true)
-
-    try {
-      // try to guess the handle if the user just gave their own username
-      let fullIdent = identifier
-      if (
-        !identifier.includes('@') && // not an email
-        !identifier.includes('.') && // not a domain
-        serviceDescription &&
-        serviceDescription.availableUserDomains.length > 0
-      ) {
-        let matched = false
-        for (const domain of serviceDescription.availableUserDomains) {
-          if (fullIdent.endsWith(domain)) {
-            matched = true
-          }
-        }
-        if (!matched) {
-          fullIdent = createFullHandle(
-            identifier,
-            serviceDescription.availableUserDomains[0],
-          )
-        }
-      }
-
-      // TODO remove double login
-      await login(
-        {
-          service: serviceUrl,
-          identifier: fullIdent,
-          password,
-        },
-        'LoginForm',
-      )
-    } catch (e: any) {
-      const errMsg = e.toString()
-      setIsProcessing(false)
-      if (errMsg.includes('Authentication Required')) {
-        logger.debug('Failed to login due to invalid credentials', {
-          error: errMsg,
-        })
-        setError(_(msg`Invalid username or password`))
-      } else if (isNetworkError(e)) {
-        logger.warn('Failed to login due to network error', {error: errMsg})
-        setError(
-          _(
-            msg`Unable to contact your service. Please check your Internet connection.`,
-          ),
-        )
-      } else {
-        logger.warn('Failed to login', {error: errMsg})
-        setError(cleanError(errMsg))
-      }
-    }
-  }
-
-  const isReady = !!serviceDescription && !!identifier && !!password
-  return (
-    <View testID="loginForm">
-      <ServerInputDialog
-        control={serverInputControl}
-        onSelect={setServiceUrl}
-      />
-
-      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
-        <Trans>Sign into</Trans>
-      </Text>
-      <View style={[pal.borderDark, styles.group]}>
-        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <FontAwesomeIcon
-            icon="globe"
-            style={[pal.textLight, styles.groupContentIcon]}
-          />
-          <TouchableOpacity
-            testID="loginSelectServiceButton"
-            style={styles.textBtn}
-            onPress={onPressSelectService}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Select service`)}
-            accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
-            <Text type="xl" style={[pal.text, styles.textBtnLabel]}>
-              {toNiceDomain(serviceUrl)}
-            </Text>
-            <View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
-              <FontAwesomeIcon
-                icon="pen"
-                size={12}
-                style={pal.textLight as FontAwesomeIconStyle}
-              />
-            </View>
-          </TouchableOpacity>
-        </View>
-      </View>
-      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
-        <Trans>Account</Trans>
-      </Text>
-      <View style={[pal.borderDark, styles.group]}>
-        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <FontAwesomeIcon
-            icon="at"
-            style={[pal.textLight, styles.groupContentIcon]}
-          />
-          <TextInput
-            testID="loginUsernameInput"
-            style={[pal.text, styles.textInput]}
-            placeholder={_(msg`Username or email address`)}
-            placeholderTextColor={pal.colors.textLight}
-            autoCapitalize="none"
-            autoFocus
-            autoCorrect={false}
-            autoComplete="username"
-            returnKeyType="next"
-            textContentType="username"
-            onSubmitEditing={() => {
-              passwordInputRef.current?.focus()
-            }}
-            blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
-            keyboardAppearance={theme.colorScheme}
-            value={identifier}
-            onChangeText={str =>
-              setIdentifier((str || '').toLowerCase().trim())
-            }
-            editable={!isProcessing}
-            accessibilityLabel={_(msg`Username or email address`)}
-            accessibilityHint={_(
-              msg`Input the username or email address you used at signup`,
-            )}
-          />
-        </View>
-        <View style={[pal.borderDark, styles.groupContent]}>
-          <FontAwesomeIcon
-            icon="lock"
-            style={[pal.textLight, styles.groupContentIcon]}
-          />
-          <TextInput
-            testID="loginPasswordInput"
-            ref={passwordInputRef}
-            style={[pal.text, styles.textInput]}
-            placeholder={_(msg`Password`)}
-            placeholderTextColor={pal.colors.textLight}
-            autoCapitalize="none"
-            autoCorrect={false}
-            autoComplete="password"
-            returnKeyType="done"
-            enablesReturnKeyAutomatically={true}
-            keyboardAppearance={theme.colorScheme}
-            secureTextEntry={true}
-            textContentType="password"
-            clearButtonMode="while-editing"
-            value={password}
-            onChangeText={setPassword}
-            onSubmitEditing={onPressNext}
-            blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
-            editable={!isProcessing}
-            accessibilityLabel={_(msg`Password`)}
-            accessibilityHint={
-              identifier === ''
-                ? _(msg`Input your password`)
-                : _(msg`Input the password tied to ${identifier}`)
-            }
-          />
-          <TouchableOpacity
-            testID="forgotPasswordButton"
-            style={styles.textInputInnerBtn}
-            onPress={onPressForgotPassword}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Forgot password`)}
-            accessibilityHint={_(msg`Opens password reset form`)}>
-            <Text style={pal.link}>
-              <Trans>Forgot</Trans>
-            </Text>
-          </TouchableOpacity>
-        </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} accessibilityRole="button">
-          <Text type="xl" style={[pal.link, s.pl5]}>
-            <Trans>Back</Trans>
-          </Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-        {!serviceDescription && error ? (
-          <TouchableOpacity
-            testID="loginRetryButton"
-            onPress={onPressRetryConnect}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Retry`)}
-            accessibilityHint={_(msg`Retries login`)}>
-            <Text type="xl-bold" style={[pal.link, s.pr5]}>
-              <Trans>Retry</Trans>
-            </Text>
-          </TouchableOpacity>
-        ) : !serviceDescription ? (
-          <>
-            <ActivityIndicator />
-            <Text type="xl" style={[pal.textLight, s.pl10]}>
-              <Trans>Connecting...</Trans>
-            </Text>
-          </>
-        ) : isProcessing ? (
-          <ActivityIndicator />
-        ) : isReady ? (
-          <TouchableOpacity
-            testID="loginNextButton"
-            onPress={onPressNext}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Go to next`)}
-            accessibilityHint={_(msg`Navigates to the next screen`)}>
-            <Text type="xl-bold" style={[pal.link, s.pr5]}>
-              <Trans>Next</Trans>
-            </Text>
-          </TouchableOpacity>
-        ) : undefined}
-      </View>
-    </View>
-  )
-}
diff --git a/src/view/com/auth/login/PasswordUpdatedForm.tsx b/src/view/com/auth/login/PasswordUpdatedForm.tsx
deleted file mode 100644
index 71f750b14..000000000
--- a/src/view/com/auth/login/PasswordUpdatedForm.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, {useEffect} from 'react'
-import {TouchableOpacity, View} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {styles} from './styles'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-export const PasswordUpdatedForm = ({
-  onPressNext,
-}: {
-  onPressNext: () => void
-}) => {
-  const {screen} = useAnalytics()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-
-  useEffect(() => {
-    screen('Signin:PasswordUpdatedForm')
-  }, [screen])
-
-  return (
-    <>
-      <View>
-        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
-          <Trans>Password updated!</Trans>
-        </Text>
-        <Text type="lg" style={[pal.text, styles.instructions]}>
-          <Trans>You can now sign in with your new password.</Trans>
-        </Text>
-        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-          <View style={s.flex1} />
-          <TouchableOpacity
-            onPress={onPressNext}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Close alert`)}
-            accessibilityHint={_(msg`Closes password update alert`)}>
-            <Text type="xl-bold" style={[pal.link, s.pr5]}>
-              <Trans>Okay</Trans>
-            </Text>
-          </TouchableOpacity>
-        </View>
-      </View>
-    </>
-  )
-}
diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx
deleted file mode 100644
index 6d1584c86..000000000
--- a/src/view/com/auth/login/SetNewPasswordForm.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {
-  ActivityIndicator,
-  TextInput,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {BskyAgent} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {isNetworkError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {cleanError} from 'lib/strings/errors'
-import {checkAndFormatResetCode} from 'lib/strings/password'
-import {logger} from '#/logger'
-import {styles} from './styles'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-export const SetNewPasswordForm = ({
-  error,
-  serviceUrl,
-  setError,
-  onPressBack,
-  onPasswordSet,
-}: {
-  error: string
-  serviceUrl: string
-  setError: (v: string) => void
-  onPressBack: () => void
-  onPasswordSet: () => void
-}) => {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const {screen} = useAnalytics()
-  const {_} = useLingui()
-
-  useEffect(() => {
-    screen('Signin:SetNewPasswordForm')
-  }, [screen])
-
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [resetCode, setResetCode] = useState<string>('')
-  const [password, setPassword] = useState<string>('')
-
-  const onPressNext = async () => {
-    // Check that the code is correct. We do this again just incase the user enters the code after their pw and we
-    // don't get to call onBlur first
-    const formattedCode = checkAndFormatResetCode(resetCode)
-    // TODO Better password strength check
-    if (!formattedCode || !password) {
-      setError(
-        _(
-          msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
-        ),
-      )
-      return
-    }
-
-    setError('')
-    setIsProcessing(true)
-
-    try {
-      const agent = new BskyAgent({service: serviceUrl})
-      await agent.com.atproto.server.resetPassword({
-        token: formattedCode,
-        password,
-      })
-      onPasswordSet()
-    } catch (e: any) {
-      const errMsg = e.toString()
-      logger.warn('Failed to set new password', {error: e})
-      setIsProcessing(false)
-      if (isNetworkError(e)) {
-        setError(
-          'Unable to contact your service. Please check your Internet connection.',
-        )
-      } else {
-        setError(cleanError(errMsg))
-      }
-    }
-  }
-
-  const onBlur = () => {
-    const formattedCode = checkAndFormatResetCode(resetCode)
-    if (!formattedCode) {
-      setError(
-        _(
-          msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
-        ),
-      )
-      return
-    }
-    setResetCode(formattedCode)
-  }
-
-  return (
-    <>
-      <View>
-        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
-          <Trans>Set new password</Trans>
-        </Text>
-        <Text type="lg" style={[pal.text, styles.instructions]}>
-          <Trans>
-            You will receive an email with a "reset code." Enter that code here,
-            then enter your new password.
-          </Trans>
-        </Text>
-        <View
-          testID="newPasswordView"
-          style={[pal.view, pal.borderDark, styles.group]}>
-          <View
-            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-            <FontAwesomeIcon
-              icon="ticket"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <TextInput
-              testID="resetCodeInput"
-              style={[pal.text, styles.textInput]}
-              placeholder={_(msg`Reset code`)}
-              placeholderTextColor={pal.colors.textLight}
-              autoCapitalize="none"
-              autoCorrect={false}
-              keyboardAppearance={theme.colorScheme}
-              autoComplete="off"
-              value={resetCode}
-              onChangeText={setResetCode}
-              onFocus={() => setError('')}
-              onBlur={onBlur}
-              editable={!isProcessing}
-              accessible={true}
-              accessibilityLabel={_(msg`Reset code`)}
-              accessibilityHint={_(
-                msg`Input code sent to your email for password reset`,
-              )}
-            />
-          </View>
-          <View style={[pal.borderDark, styles.groupContent]}>
-            <FontAwesomeIcon
-              icon="lock"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <TextInput
-              testID="newPasswordInput"
-              style={[pal.text, styles.textInput]}
-              placeholder={_(msg`New password`)}
-              placeholderTextColor={pal.colors.textLight}
-              autoCapitalize="none"
-              autoCorrect={false}
-              autoComplete="new-password"
-              keyboardAppearance={theme.colorScheme}
-              secureTextEntry
-              value={password}
-              onChangeText={setPassword}
-              editable={!isProcessing}
-              accessible={true}
-              accessibilityLabel={_(msg`Password`)}
-              accessibilityHint={_(msg`Input new password`)}
-            />
-          </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} accessibilityRole="button">
-            <Text type="xl" style={[pal.link, s.pl5]}>
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {isProcessing ? (
-            <ActivityIndicator />
-          ) : !resetCode || !password ? (
-            <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
-              <Trans>Next</Trans>
-            </Text>
-          ) : (
-            <TouchableOpacity
-              testID="setNewPasswordButton"
-              // Check the code before running the callback
-              onPress={onPressNext}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Go to next`)}
-              accessibilityHint={_(msg`Navigates to the next screen`)}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                <Trans>Next</Trans>
-              </Text>
-            </TouchableOpacity>
-          )}
-          {isProcessing ? (
-            <Text type="xl" style={[pal.textLight, s.pl10]}>
-              <Trans>Updating...</Trans>
-            </Text>
-          ) : undefined}
-        </View>
-      </View>
-    </>
-  )
-}
diff --git a/src/view/com/auth/login/styles.ts b/src/view/com/auth/login/styles.ts
deleted file mode 100644
index 9dccc2803..000000000
--- a/src/view/com/auth/login/styles.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import {StyleSheet} from 'react-native'
-import {colors} from 'lib/styles'
-import {isWeb} from '#/platform/detection'
-
-export const styles = StyleSheet.create({
-  screenTitle: {
-    marginBottom: 10,
-    marginHorizontal: 20,
-  },
-  instructions: {
-    marginBottom: 20,
-    marginHorizontal: 20,
-  },
-  group: {
-    borderWidth: 1,
-    borderRadius: 10,
-    marginBottom: 20,
-    marginHorizontal: 20,
-  },
-  groupLabel: {
-    paddingHorizontal: 20,
-    paddingBottom: 5,
-  },
-  groupContent: {
-    borderTopWidth: 1,
-    flexDirection: 'row',
-    alignItems: 'center',
-  },
-  noTopBorder: {
-    borderTopWidth: 0,
-  },
-  groupContentIcon: {
-    marginLeft: 10,
-  },
-  account: {
-    borderTopWidth: 1,
-    paddingHorizontal: 20,
-    paddingVertical: 4,
-  },
-  accountLast: {
-    borderBottomWidth: 1,
-    marginBottom: 20,
-    paddingVertical: 8,
-  },
-  textInput: {
-    flex: 1,
-    width: '100%',
-    paddingVertical: 10,
-    paddingHorizontal: 12,
-    fontSize: 17,
-    letterSpacing: 0.25,
-    fontWeight: '400',
-    borderRadius: 10,
-  },
-  textInputInnerBtn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 6,
-    paddingHorizontal: 8,
-    marginHorizontal: 6,
-  },
-  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,
-  },
-  accountText: {
-    flex: 1,
-    flexDirection: 'row',
-    alignItems: 'baseline',
-    paddingVertical: 10,
-  },
-  accountTextOther: {
-    paddingLeft: 12,
-  },
-  error: {
-    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,
-  },
-  dimmed: {opacity: 0.5},
-
-  maxHeight: {
-    // @ts-ignore web only -prf
-    maxHeight: isWeb ? '100vh' : undefined,
-    height: !isWeb ? '100%' : undefined,
-  },
-})