about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/alf/atoms.ts69
-rw-r--r--src/components/forms/HostingProvider.tsx69
-rw-r--r--src/components/icons/Ticket.tsx5
-rw-r--r--src/screens/Login/ChooseAccountForm.tsx43
-rw-r--r--src/screens/Login/ForgotPasswordForm.tsx183
-rw-r--r--src/screens/Login/FormContainer.tsx52
-rw-r--r--src/screens/Login/FormError.tsx34
-rw-r--r--src/screens/Login/LoginForm.tsx297
-rw-r--r--src/screens/Login/PasswordUpdatedForm.tsx49
-rw-r--r--src/screens/Login/SetNewPasswordForm.tsx189
-rw-r--r--src/screens/Login/index.tsx6
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx228
-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
15 files changed, 802 insertions, 799 deletions
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index fef68ecab..33803b6fb 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -300,6 +300,9 @@ export const atoms = {
   /*
    * Padding
    */
+  p_0: {
+    padding: 0,
+  },
   p_2xs: {
     padding: tokens.space._2xs,
   },
@@ -330,6 +333,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 +377,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 +421,9 @@ export const atoms = {
     paddingTop: tokens.space._5xl,
     paddingBottom: tokens.space._5xl,
   },
+  pt_0: {
+    paddingTop: 0,
+  },
   pt_2xs: {
     paddingTop: tokens.space._2xs,
   },
@@ -440,6 +454,9 @@ export const atoms = {
   pt_5xl: {
     paddingTop: tokens.space._5xl,
   },
+  pb_0: {
+    paddingBottom: 0,
+  },
   pb_2xs: {
     paddingBottom: tokens.space._2xs,
   },
@@ -470,6 +487,9 @@ export const atoms = {
   pb_5xl: {
     paddingBottom: tokens.space._5xl,
   },
+  pl_0: {
+    paddingLeft: 0,
+  },
   pl_2xs: {
     paddingLeft: tokens.space._2xs,
   },
@@ -500,6 +520,9 @@ export const atoms = {
   pl_5xl: {
     paddingLeft: tokens.space._5xl,
   },
+  pr_0: {
+    paddingRight: 0,
+  },
   pr_2xs: {
     paddingRight: tokens.space._2xs,
   },
@@ -534,6 +557,9 @@ export const atoms = {
   /*
    * Margin
    */
+  m_0: {
+    margin: 0,
+  },
   m_2xs: {
     margin: tokens.space._2xs,
   },
@@ -564,6 +590,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,
@@ -604,6 +637,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,
@@ -644,6 +685,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,
   },
@@ -674,6 +722,12 @@ export const atoms = {
   mt_5xl: {
     marginTop: tokens.space._5xl,
   },
+  mt_auto: {
+    marginTop: 'auto',
+  },
+  mb_0: {
+    marginBottom: 0,
+  },
   mb_2xs: {
     marginBottom: tokens.space._2xs,
   },
@@ -704,6 +758,12 @@ export const atoms = {
   mb_5xl: {
     marginBottom: tokens.space._5xl,
   },
+  mb_auto: {
+    marginBottom: 'auto',
+  },
+  ml_0: {
+    marginLeft: 0,
+  },
   ml_2xs: {
     marginLeft: tokens.space._2xs,
   },
@@ -734,6 +794,12 @@ export const atoms = {
   ml_5xl: {
     marginLeft: tokens.space._5xl,
   },
+  ml_auto: {
+    marginLeft: 'auto',
+  },
+  mr_0: {
+    marginRight: 0,
+  },
   mr_2xs: {
     marginRight: tokens.space._2xs,
   },
@@ -764,4 +830,7 @@ export const atoms = {
   mr_5xl: {
     marginRight: tokens.space._5xl,
   },
+  mr_auto: {
+    marginRight: 'auto',
+  },
 } as const
diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx
new file mode 100644
index 000000000..df506b77c
--- /dev/null
+++ b/src/components/forms/HostingProvider.tsx
@@ -0,0 +1,69 @@
+import React from 'react'
+import {TouchableOpacity, View} from 'react-native'
+
+import {isAndroid} from '#/platform/detection'
+import {atoms as a, useTheme} from '#/alf'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
+import * as TextField from './TextField'
+import {useDialogControl} from '../Dialog'
+import {Text} from '../Typography'
+import {ServerInputDialog} from '#/view/com/auth/server-input'
+import {toNiceDomain} from '#/lib/strings/url-helpers'
+
+export function HostingProvider({
+  serviceUrl,
+  onSelectServiceUrl,
+  onOpenDialog,
+}: {
+  serviceUrl: string
+  onSelectServiceUrl: (provider: string) => void
+  onOpenDialog?: () => void
+}) {
+  const serverInputControl = useDialogControl()
+  const t = useTheme()
+
+  const onPressSelectService = React.useCallback(() => {
+    serverInputControl.open()
+    if (onOpenDialog) {
+      onOpenDialog()
+    }
+  }, [onOpenDialog, serverInputControl])
+
+  return (
+    <>
+      <ServerInputDialog
+        control={serverInputControl}
+        onSelect={onSelectServiceUrl}
+      />
+      <TouchableOpacity
+        accessibilityRole="button"
+        style={[
+          a.w_full,
+          a.flex_row,
+          a.align_center,
+          a.rounded_sm,
+          a.px_md,
+          a.gap_xs,
+          {paddingVertical: isAndroid ? 14 : 9},
+          t.atoms.bg_contrast_25,
+        ]}
+        onPress={onPressSelectService}>
+        <TextField.Icon icon={Globe} />
+        <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
+        <View
+          style={[
+            a.rounded_sm,
+            t.atoms.bg_contrast_100,
+            {marginLeft: 'auto', left: 6, padding: 6},
+          ]}>
+          <Pencil
+            style={{color: t.palette.contrast_500}}
+            height={18}
+            width={18}
+          />
+        </View>
+      </TouchableOpacity>
+    </>
+  )
+}
diff --git a/src/components/icons/Ticket.tsx b/src/components/icons/Ticket.tsx
new file mode 100644
index 000000000..0df6b8120
--- /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 12c0-1.296.704-2.426 1.75-3.032a.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/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
index f5b3c2a86..7a3a4555b 100644
--- a/src/screens/Login/ChooseAccountForm.tsx
+++ b/src/screens/Login/ChooseAccountForm.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {ScrollView, TouchableOpacity, View} from 'react-native'
+import {TouchableOpacity, View} from 'react-native'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import flattenReactChildren from 'react-keyed-flatten-children'
@@ -7,16 +7,17 @@ import flattenReactChildren from 'react-keyed-flatten-children'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {UserAvatar} from '../../view/com/util/UserAvatar'
 import {colors} from 'lib/styles'
-import {styles} from '../../view/com/auth/login/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 {Button} from '#/components/Button'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 import {Text} from '#/components/Typography'
 import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import * as TextField from '#/components/forms/TextField'
+import {FormContainer} from './FormContainer'
 
 function Group({children}: {children: React.ReactNode}) {
   const t = useTheme()
@@ -106,7 +107,6 @@ export const ChooseAccountForm = ({
   const {accounts, currentAccount} = useSession()
   const {initSession} = useSessionApi()
   const {setShowLoggedOut} = useLoggedOutViewControls()
-  const {gtMobile} = useBreakpoints()
 
   React.useEffect(() => {
     screen('Choose Account')
@@ -133,12 +133,13 @@ export const ChooseAccountForm = ({
   )
 
   return (
-    <ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
-      <View style={!gtMobile && a.px_lg}>
-        <Text
-          style={[a.mt_md, a.mb_lg, a.font_bold, t.atoms.text_contrast_medium]}>
+    <FormContainer
+      testID="chooseAccountForm"
+      title={<Trans>Select account</Trans>}>
+      <View>
+        <TextField.Label>
           <Trans>Sign in as...</Trans>
-        </Text>
+        </TextField.Label>
         <Group>
           {accounts.map(account => (
             <AccountItem
@@ -171,18 +172,18 @@ export const ChooseAccountForm = ({
             </View>
           </TouchableOpacity>
         </Group>
-        <View style={[a.flex_row, a.mt_lg]}>
-          <Button
-            label={_(msg`Back`)}
-            variant="solid"
-            color="secondary"
-            size="small"
-            onPress={onPressBack}>
-            {_(msg`Back`)}
-          </Button>
-          <View style={[a.flex_1]} />
-        </View>
       </View>
-    </ScrollView>
+      <View style={[a.flex_row]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="small"
+          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..fa674155a
--- /dev/null
+++ b/src/screens/Login/ForgotPasswordForm.tsx
@@ -0,0 +1,183 @@
+import React, {useState, useEffect} from 'react'
+import {ActivityIndicator, Keyboard, View} from 'react-native'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
+import * as EmailValidator from 'email-validator'
+import {BskyAgent} from '@atproto/api'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import * as TextField from '#/components/forms/TextField'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
+import {atoms as a, useTheme} from '#/alf'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {isNetworkError} from 'lib/strings/errors'
+import {cleanError} from 'lib/strings/errors'
+import {logger} from '#/logger'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+import {FormError} from './FormError'
+
+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>
+      <View>
+        <Text style={[t.atoms.text_contrast_high, a.mb_md]}>
+          <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>
+      <FormError error={error} />
+      <View style={[a.flex_row, a.align_center]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="small"
+          onPress={onPressBack}>
+          <ButtonText>
+            <Trans>Back</Trans>
+          </ButtonText>
+        </Button>
+        <View style={a.flex_1} />
+        {!serviceDescription || isProcessing ? (
+          <ActivityIndicator />
+        ) : (
+          <Button
+            label={_(msg`Next`)}
+            variant="solid"
+            color={email ? 'primary' : 'secondary'}
+            size="small"
+            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="small"
+          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..a08aa05b0
--- /dev/null
+++ b/src/screens/Login/FormContainer.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import {
+  ScrollView,
+  StyleSheet,
+  View,
+  type StyleProp,
+  type ViewStyle,
+} from 'react-native'
+
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {isWeb} from '#/platform/detection'
+
+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]}>
+      <View
+        style={[a.gap_lg, 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/FormError.tsx b/src/screens/Login/FormError.tsx
new file mode 100644
index 000000000..3c6a8649d
--- /dev/null
+++ b/src/screens/Login/FormError.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+
+import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import {Text} from '#/components/Typography'
+import {atoms as a, useTheme} from '#/alf'
+import {colors} from '#/lib/styles'
+
+export function FormError({error}: {error?: string}) {
+  const t = useTheme()
+
+  if (!error) return null
+
+  return (
+    <View style={styles.error}>
+      <Warning fill={t.palette.white} size="sm" />
+      <View style={(a.flex_1, a.ml_sm)}>
+        <Text style={[{color: t.palette.white}, a.font_bold]}>{error}</Text>
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  error: {
+    backgroundColor: colors.red4,
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 15,
+    borderRadius: 8,
+    paddingHorizontal: 8,
+    paddingVertical: 8,
+  },
+})
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index 3089b3887..580155281 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -2,36 +2,29 @@ import React, {useState, useRef} from 'react'
 import {
   ActivityIndicator,
   Keyboard,
-  ScrollView,
   TextInput,
   TouchableOpacity,
   View,
 } from 'react-native'
 import {ComAtprotoServerDescribeServer} from '@atproto/api'
 import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 import {useAnalytics} from 'lib/analytics/analytics'
-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 {useSessionApi} from '#/state/session'
 import {cleanError} from 'lib/strings/errors'
 import {logger} from '#/logger'
-import {styles} from '../../view/com/auth/login/styles'
-import {useLingui} from '@lingui/react'
-import {useDialogControl} from '#/components/Dialog'
-import {ServerInputDialog} from '../../view/com/auth/server-input'
 import {Button, ButtonText} from '#/components/Button'
-import {isAndroid} from '#/platform/detection'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 import {Text} from '#/components/Typography'
 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 {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
-import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
-import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import {FormContainer} from './FormContainer'
+import {FormError} from './FormError'
 
 type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
 
@@ -64,14 +57,11 @@ export const LoginForm = ({
   const passwordInputRef = useRef<TextInput>(null)
   const {_} = useLingui()
   const {login} = useSessionApi()
-  const serverInputControl = useDialogControl()
-  const {gtMobile} = useBreakpoints()
 
-  const onPressSelectService = () => {
-    serverInputControl.open()
+  const onPressSelectService = React.useCallback(() => {
     Keyboard.dismiss()
     track('Signin:PressedSelectService')
-  }
+  }, [track])
 
   const onPressNext = async () => {
     Keyboard.dismiss()
@@ -131,171 +121,138 @@ export const LoginForm = ({
 
   const isReady = !!serviceDescription && !!identifier && !!password
   return (
-    <ScrollView testID="loginForm" style={a.h_full}>
-      <View style={[a.gap_lg, !gtMobile && a.px_lg, a.flex_1]}>
-        <ServerInputDialog
-          control={serverInputControl}
-          onSelect={setServiceUrl}
+    <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>
-          <TextField.Label>
-            <Trans>Hosting provider</Trans>
-          </TextField.Label>
+      </View>
+      <View>
+        <TextField.Label>
+          <Trans>Account</Trans>
+        </TextField.Label>
+        <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>
+      </View>
+      <View>
+        <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}`)
+            }
+          />
           <TouchableOpacity
+            testID="forgotPasswordButton"
+            onPress={onPressForgotPassword}
             accessibilityRole="button"
+            accessibilityLabel={_(msg`Forgot password`)}
+            accessibilityHint={_(msg`Opens password reset form`)}
             style={[
-              a.w_full,
-              a.flex_row,
-              a.align_center,
               a.rounded_sm,
-              a.px_md,
-              a.gap_xs,
-              {paddingVertical: isAndroid ? 14 : 9},
-              t.atoms.bg_contrast_25,
-            ]}
-            onPress={onPressSelectService}>
-            <TextField.Icon icon={Globe} />
-            <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
-            <View
-              style={[
-                a.rounded_sm,
-                t.atoms.bg_contrast_100,
-                {marginLeft: 'auto', left: 6, padding: 6},
-              ]}>
-              <Pencil
-                style={{color: t.palette.contrast_500}}
-                height={18}
-                width={18}
-              />
-            </View>
+              t.atoms.bg_contrast_100,
+              {marginLeft: 'auto', left: 6, padding: 6},
+              a.z_10,
+            ]}>
+            <ButtonText style={t.atoms.text_contrast_medium}>
+              <Trans>Forgot?</Trans>
+            </ButtonText>
           </TouchableOpacity>
-        </View>
-        <View>
-          <TextField.Label>
-            <Trans>Account</Trans>
-          </TextField.Label>
-          <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>
-        </View>
-        <View>
-          <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}`)
-              }
-            />
-            <TouchableOpacity
-              testID="forgotPasswordButton"
-              onPress={onPressForgotPassword}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Forgot password`)}
-              accessibilityHint={_(msg`Opens password reset form`)}
-              style={[
-                a.rounded_sm,
-                t.atoms.bg_contrast_100,
-                {marginLeft: 'auto', left: 6, padding: 6},
-                a.z_10,
-              ]}>
-              <ButtonText style={t.atoms.text_contrast_medium}>
-                <Trans>Forgot?</Trans>
-              </ButtonText>
-            </TouchableOpacity>
-          </TextField.Root>
-        </View>
-        {error ? (
-          <View style={[styles.error, {marginHorizontal: 0}]}>
-            <Warning style={s.white} size="sm" />
-            <View style={(a.flex_1, a.ml_sm)}>
-              <Text style={[s.white, s.bold]}>{error}</Text>
-            </View>
-          </View>
-        ) : undefined}
-        <View style={[a.flex_row, a.align_center]}>
+        </TextField.Root>
+      </View>
+      <FormError error={error} />
+      <View style={[a.flex_row, a.align_center]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="small"
+          onPress={onPressBack}>
+          <ButtonText>
+            <Trans>Back</Trans>
+          </ButtonText>
+        </Button>
+        <View style={a.flex_1} />
+        {!serviceDescription && error ? (
           <Button
-            label={_(msg`Back`)}
+            testID="loginRetryButton"
+            label={_(msg`Retry`)}
+            accessibilityHint={_(msg`Retries login`)}
             variant="solid"
             color="secondary"
             size="small"
-            onPress={onPressBack}>
-            {_(msg`Back`)}
+            onPress={onPressRetryConnect}>
+            {_(msg`Retry`)}
           </Button>
-          <View style={s.flex1} />
-          {!serviceDescription && error ? (
-            <Button
-              testID="loginRetryButton"
-              label={_(msg`Retry`)}
-              accessibilityHint={_(msg`Retries login`)}
-              variant="solid"
-              color="secondary"
-              size="small"
-              onPress={onPressRetryConnect}>
-              {_(msg`Retry`)}
-            </Button>
-          ) : !serviceDescription ? (
-            <>
-              <ActivityIndicator />
-              <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
-                <Trans>Connecting...</Trans>
-              </Text>
-            </>
-          ) : isProcessing ? (
+        ) : !serviceDescription ? (
+          <>
             <ActivityIndicator />
-          ) : isReady ? (
-            <Button
-              label={_(msg`Next`)}
-              accessibilityHint={_(msg`Navigates to the next screen`)}
-              variant="solid"
-              color="primary"
-              size="small"
-              onPress={onPressNext}>
-              {_(msg`Next`)}
-            </Button>
-          ) : undefined}
-        </View>
+            <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
+              <Trans>Connecting...</Trans>
+            </Text>
+          </>
+        ) : isProcessing ? (
+          <ActivityIndicator />
+        ) : isReady ? (
+          <Button
+            label={_(msg`Next`)}
+            accessibilityHint={_(msg`Navigates to the next screen`)}
+            variant="solid"
+            color="primary"
+            size="small"
+            onPress={onPressNext}>
+            <ButtonText>
+              <Trans>Next</Trans>
+            </ButtonText>
+          </Button>
+        ) : undefined}
       </View>
-    </ScrollView>
+    </FormContainer>
   )
 }
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
new file mode 100644
index 000000000..218cab539
--- /dev/null
+++ b/src/screens/Login/PasswordUpdatedForm.tsx
@@ -0,0 +1,49 @@
+import React, {useEffect} from 'react'
+import {View} from 'react-native'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {FormContainer} from './FormContainer'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {atoms as a, useBreakpoints} from '#/alf'
+
+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/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx
new file mode 100644
index 000000000..2685ad5ee
--- /dev/null
+++ b/src/screens/Login/SetNewPasswordForm.tsx
@@ -0,0 +1,189 @@
+import React, {useState, useEffect} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {BskyAgent} from '@atproto/api'
+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 {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {FormContainer} from './FormContainer'
+import {Text} from '#/components/Typography'
+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 {Button, ButtonText} from '#/components/Button'
+import {useTheme, atoms as a} from '#/alf'
+import {FormError} from './FormError'
+
+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 () => {
+    onPasswordSet()
+    if (Math.random() > 0) return
+    // 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 (
+    <FormContainer
+      testID="setNewPasswordForm"
+      title={<Trans>Set new password</Trans>}>
+      <Text>
+        <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"
+            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]}>
+        <Button
+          label={_(msg`Back`)}
+          variant="solid"
+          color="secondary"
+          size="small"
+          onPress={onPressBack}>
+          <ButtonText>
+            <Trans>Back</Trans>
+          </ButtonText>
+        </Button>
+        <View style={a.flex_1} />
+        {isProcessing ? (
+          <ActivityIndicator />
+        ) : (
+          <Button
+            label={_(msg`Next`)}
+            variant="solid"
+            color="primary"
+            size="small"
+            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
index 028a497d2..10edb3eb6 100644
--- a/src/screens/Login/index.tsx
+++ b/src/screens/Login/index.tsx
@@ -13,9 +13,9 @@ import {msg} from '@lingui/macro'
 import {logger} from '#/logger'
 import {atoms as a} from '#/alf'
 import {ChooseAccountForm} from './ChooseAccountForm'
-import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm'
-import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm'
-import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm'
+import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
+import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
+import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
 import {LoginForm} from '#/screens/Login/LoginForm'
 
 enum Forms {
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/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,
-  },
-})