about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/index.ts4
-rw-r--r--src/state/models/session.ts218
-rw-r--r--src/view/com/login/CreateAccount.tsx327
-rw-r--r--src/view/com/login/Logo.tsx42
-rw-r--r--src/view/com/login/Signin.tsx589
-rw-r--r--src/view/com/modals/ServerInput.tsx4
-rw-r--r--src/view/com/util/ViewHeader.tsx1
-rw-r--r--src/view/lib/ThemeContext.tsx1
-rw-r--r--src/view/lib/hooks/usePalette.ts4
-rw-r--r--src/view/lib/themes.ts7
-rw-r--r--src/view/screens/Login.tsx73
-rw-r--r--src/view/screens/Settings.tsx116
-rw-r--r--src/view/shell/mobile/Menu.tsx2
-rw-r--r--src/view/shell/mobile/index.tsx18
14 files changed, 925 insertions, 481 deletions
diff --git a/src/state/index.ts b/src/state/index.ts
index 5c8b50ef1..78fba2ecf 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -13,13 +13,13 @@ export const DEFAULT_SERVICE = PROD_SERVICE
 const ROOT_STATE_STORAGE_KEY = 'root'
 const STATE_FETCH_INTERVAL = 15e3
 
-export async function setupState() {
+export async function setupState(serviceUri = DEFAULT_SERVICE) {
   let rootStore: RootStoreModel
   let data: any
 
   libapi.doPolyfill()
 
-  const api = AtpApi.service(DEFAULT_SERVICE) as SessionServiceClient
+  const api = AtpApi.service(serviceUri) as SessionServiceClient
   rootStore = new RootStoreModel(api)
   try {
     data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 13e0fcbe0..89347af9a 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -6,24 +6,44 @@ import {
   ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
 } from '@atproto/api'
 import {isObj, hasProp} from '../lib/type-guards'
+import {z} from 'zod'
 import {RootStoreModel} from './root-store'
 import {isNetworkError} from '../../lib/errors'
 
 export type ServiceDescription = GetAccountsConfig.OutputSchema
 
-interface SessionData {
-  service: string
-  refreshJwt: string
-  accessJwt: string
-  handle: string
-  did: string
-}
+export const sessionData = z.object({
+  service: z.string(),
+  refreshJwt: z.string(),
+  accessJwt: z.string(),
+  handle: z.string(),
+  did: z.string(),
+})
+export type SessionData = z.infer<typeof sessionData>
+
+export const accountData = z.object({
+  service: z.string(),
+  refreshJwt: z.string().optional(),
+  accessJwt: z.string().optional(),
+  handle: z.string(),
+  did: z.string(),
+  displayName: z.string().optional(),
+  aviUrl: z.string().optional(),
+})
+export type AccountData = z.infer<typeof accountData>
 
 export class SessionModel {
+  /**
+   * Current session data
+   */
   data: SessionData | null = null
+  /**
+   * A listing of the currently & previous sessions, used for account switching
+   */
+  accounts: AccountData[] = []
   online = false
   attemptingConnect = false
-  private _connectPromise: Promise<void> | undefined
+  private _connectPromise: Promise<boolean> | undefined
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {
@@ -37,51 +57,32 @@ export class SessionModel {
     return this.data !== null
   }
 
+  get hasAccounts() {
+    return this.accounts.length >= 1
+  }
+
+  get switchableAccounts() {
+    return this.accounts.filter(acct => acct.did !== this.data?.did)
+  }
+
   serialize(): unknown {
     return {
       data: this.data,
+      accounts: this.accounts,
     }
   }
 
   hydrate(v: unknown) {
+    this.accounts = []
     if (isObj(v)) {
-      if (hasProp(v, 'data') && isObj(v.data)) {
-        const data: SessionData = {
-          service: '',
-          refreshJwt: '',
-          accessJwt: '',
-          handle: '',
-          did: '',
-        }
-        if (hasProp(v.data, 'service') && typeof v.data.service === 'string') {
-          data.service = v.data.service
-        }
-        if (
-          hasProp(v.data, 'refreshJwt') &&
-          typeof v.data.refreshJwt === 'string'
-        ) {
-          data.refreshJwt = v.data.refreshJwt
-        }
-        if (
-          hasProp(v.data, 'accessJwt') &&
-          typeof v.data.accessJwt === 'string'
-        ) {
-          data.accessJwt = v.data.accessJwt
-        }
-        if (hasProp(v.data, 'handle') && typeof v.data.handle === 'string') {
-          data.handle = v.data.handle
-        }
-        if (hasProp(v.data, 'did') && typeof v.data.did === 'string') {
-          data.did = v.data.did
-        }
-        if (
-          data.service &&
-          data.refreshJwt &&
-          data.accessJwt &&
-          data.handle &&
-          data.did
-        ) {
-          this.data = data
+      if (hasProp(v, 'data') && sessionData.safeParse(v.data)) {
+        this.data = v.data as SessionData
+      }
+      if (hasProp(v, 'accounts') && Array.isArray(v.accounts)) {
+        for (const account of v.accounts) {
+          if (accountData.safeParse(account)) {
+            this.accounts.push(account as AccountData)
+          }
         }
       }
     }
@@ -113,6 +114,9 @@ export class SessionModel {
     }
   }
 
+  /**
+   * Sets up the XRPC API, must be called before connecting to a service
+   */
   private configureApi(): boolean {
     if (!this.data) {
       return false
@@ -137,19 +141,68 @@ export class SessionModel {
     return true
   }
 
-  async connect(): Promise<void> {
+  /**
+   * Upserts the current session into the accounts
+   */
+  private addSessionToAccounts() {
+    if (!this.data) {
+      return
+    }
+    const existingAccount = this.accounts.find(
+      acc => acc.service === this.data?.service && acc.did === this.data.did,
+    )
+    const newAccount = {
+      service: this.data.service,
+      refreshJwt: this.data.refreshJwt,
+      accessJwt: this.data.accessJwt,
+      handle: this.data.handle,
+      did: this.data.did,
+      displayName: this.rootStore.me.displayName,
+      aviUrl: this.rootStore.me.avatar,
+    }
+    if (!existingAccount) {
+      this.accounts.push(newAccount)
+    } else {
+      this.accounts = this.accounts
+        .filter(
+          acc =>
+            !(acc.service === this.data?.service && acc.did === this.data.did),
+        )
+        .concat([newAccount])
+    }
+  }
+
+  /**
+   * Clears any session tokens from the accounts; used on logout.
+   */
+  private clearSessionTokensFromAccounts() {
+    this.accounts = this.accounts.map(acct => ({
+      service: acct.service,
+      handle: acct.handle,
+      did: acct.did,
+      displayName: acct.displayName,
+      aviUrl: acct.aviUrl,
+    }))
+  }
+
+  /**
+   * Fetches the current session from the service, if possible.
+   * Requires an existing session (.data) to be populated with access tokens.
+   */
+  async connect(): Promise<boolean> {
     if (this._connectPromise) {
       return this._connectPromise
     }
     this._connectPromise = this._connect()
-    await this._connectPromise
+    const res = await this._connectPromise
     this._connectPromise = undefined
+    return res
   }
 
-  private async _connect(): Promise<void> {
+  private async _connect(): Promise<boolean> {
     this.attemptingConnect = true
     if (!this.configureApi()) {
-      return
+      return false
     }
 
     try {
@@ -159,29 +212,44 @@ export class SessionModel {
         if (this.rootStore.me.did !== sess.data.did) {
           this.rootStore.me.clear()
         }
-        this.rootStore.me.load().catch(e => {
-          this.rootStore.log.error('Failed to fetch local user information', e)
-        })
-        return // success
+        this.rootStore.me
+          .load()
+          .catch(e => {
+            this.rootStore.log.error(
+              'Failed to fetch local user information',
+              e,
+            )
+          })
+          .then(() => {
+            this.addSessionToAccounts()
+          })
+        return true // success
       }
     } catch (e: any) {
       if (isNetworkError(e)) {
         this.setOnline(false, false) // connection issue
-        return
+        return false
       } else {
         this.clear() // invalid session cached
       }
     }
 
     this.setOnline(false, false)
+    return false
   }
 
+  /**
+   * Helper to fetch the accounts config settings from an account.
+   */
   async describeService(service: string): Promise<ServiceDescription> {
     const api = AtpApi.service(service) as SessionServiceClient
     const res = await api.com.atproto.server.getAccountsConfig({})
     return res.data
   }
 
+  /**
+   * Create a new session.
+   */
   async login({
     service,
     handle,
@@ -203,10 +271,33 @@ export class SessionModel {
       })
       this.configureApi()
       this.setOnline(true, false)
-      this.rootStore.me.load().catch(e => {
-        this.rootStore.log.error('Failed to fetch local user information', e)
+      this.rootStore.me
+        .load()
+        .catch(e => {
+          this.rootStore.log.error('Failed to fetch local user information', e)
+        })
+        .then(() => {
+          this.addSessionToAccounts()
+        })
+    }
+  }
+
+  /**
+   * Attempt to resume a session that we still have access tokens for.
+   */
+  async resumeSession(account: AccountData): Promise<boolean> {
+    if (account.accessJwt && account.refreshJwt) {
+      this.setState({
+        service: account.service,
+        accessJwt: account.accessJwt,
+        refreshJwt: account.refreshJwt,
+        handle: account.handle,
+        did: account.did,
       })
+    } else {
+      return false
     }
+    return this.connect()
   }
 
   async createAccount({
@@ -239,12 +330,20 @@ export class SessionModel {
       })
       this.rootStore.onboard.start()
       this.configureApi()
-      this.rootStore.me.load().catch(e => {
-        this.rootStore.log.error('Failed to fetch local user information', e)
-      })
+      this.rootStore.me
+        .load()
+        .catch(e => {
+          this.rootStore.log.error('Failed to fetch local user information', e)
+        })
+        .then(() => {
+          this.addSessionToAccounts()
+        })
     }
   }
 
+  /**
+   * Close all sessions across all accounts.
+   */
   async logout() {
     if (this.hasSession) {
       this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
@@ -254,6 +353,7 @@ export class SessionModel {
         )
       })
     }
+    this.clearSessionTokensFromAccounts()
     this.rootStore.clearAll()
   }
 }
diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx
index 349c48ef7..6c597408f 100644
--- a/src/view/com/login/CreateAccount.tsx
+++ b/src/view/com/login/CreateAccount.tsx
@@ -12,7 +12,7 @@ import {
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {ComAtprotoAccountCreate} from '@atproto/api'
 import * as EmailValidator from 'email-validator'
-import {Logo} from './Logo'
+import {LogoTextHero} from './Logo'
 import {Picker} from '../util/Picker'
 import {TextLink} from '../util/Link'
 import {Text} from '../util/text/Text'
@@ -25,8 +25,10 @@ import {
 import {useStores, DEFAULT_SERVICE} from '../../../state'
 import {ServiceDescription} from '../../../state/models/session'
 import {ServerInputModal} from '../../../state/models/shell-ui'
+import {usePalette} from '../../lib/hooks/usePalette'
 
 export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
+  const pal = usePalette('default')
   const store = useStores()
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
@@ -114,74 +116,14 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     }
   }
 
-  const Policies = () => {
-    if (!serviceDescription) {
-      return <View />
-    }
-    const tos = validWebLink(serviceDescription.links?.termsOfService)
-    const pp = validWebLink(serviceDescription.links?.privacyPolicy)
-    if (!tos && !pp) {
-      return (
-        <View style={styles.policies}>
-          <View style={[styles.errorIcon, s.mt2]}>
-            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-          </View>
-          <Text style={[s.white, s.pl5, s.flex1]}>
-            This service has not provided terms of service or a privacy policy.
-          </Text>
-        </View>
-      )
-    }
-    const els = []
-    if (tos) {
-      els.push(
-        <TextLink
-          key="tos"
-          href={tos}
-          text="Terms of Service"
-          style={[s.white, s.underline]}
-        />,
-      )
-    }
-    if (pp) {
-      els.push(
-        <TextLink
-          key="pp"
-          href={pp}
-          text="Privacy Policy"
-          style={[s.white, s.underline]}
-        />,
-      )
-    }
-    if (els.length === 2) {
-      els.splice(
-        1,
-        0,
-        <Text key="and" style={s.white}>
-          {' '}
-          and{' '}
-        </Text>,
-      )
-    }
-    return (
-      <View style={styles.policies}>
-        <Text style={s.white}>
-          By creating an account you agree to the {els}.
-        </Text>
-      </View>
-    )
-  }
-
   const isReady = !!email && !!password && !!handle && is13
   return (
-    <ScrollView testID="createAccount" style={{flex: 1}}>
-      <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
-        <View style={styles.logoHero}>
-          <Logo />
-        </View>
+    <ScrollView testID="createAccount" style={pal.view}>
+      <KeyboardAvoidingView behavior="padding">
+        <LogoTextHero />
         {error ? (
           <View style={[styles.error, styles.errorFloating]}>
-            <View style={styles.errorIcon}>
+            <View style={[styles.errorIcon]}>
               <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
             </View>
             <View style={s.flex1}>
@@ -189,41 +131,55 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
             </View>
           </View>
         ) : undefined}
-        <View style={[styles.group]}>
-          <View style={styles.groupTitle}>
-            <Text style={[s.white, s.f18, s.bold]}>Create a new account</Text>
-          </View>
-          <View style={styles.groupContent}>
-            <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
+        <View style={styles.groupLabel}>
+          <Text type="sm-bold" style={pal.text}>
+            Service provider
+          </Text>
+        </View>
+        <View style={[pal.borderDark, styles.group]}>
+          <View
+            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
+            <FontAwesomeIcon
+              icon="globe"
+              style={[pal.textLight, styles.groupContentIcon]}
+            />
             <TouchableOpacity
               testID="registerSelectServiceButton"
               style={styles.textBtn}
               onPress={onPressSelectService}>
-              <Text style={styles.textBtnLabel}>
+              <Text type="xl" style={[pal.text, styles.textBtnLabel]}>
                 {toNiceDomain(serviceUrl)}
               </Text>
-              <View style={styles.textBtnFakeInnerBtn}>
+              <View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
                 <FontAwesomeIcon
                   icon="pen"
                   size={12}
-                  style={styles.textBtnFakeInnerBtnIcon}
+                  style={[pal.textLight, styles.textBtnFakeInnerBtnIcon]}
                 />
-                <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
+                <Text style={[pal.textLight]}>Change</Text>
               </View>
             </TouchableOpacity>
           </View>
-          {serviceDescription ? (
-            <>
+        </View>
+        {serviceDescription ? (
+          <>
+            <View style={styles.groupLabel}>
+              <Text type="sm-bold" style={pal.text}>
+                Account details
+              </Text>
+            </View>
+            <View style={[pal.borderDark, styles.group]}>
               {serviceDescription?.inviteCodeRequired ? (
-                <View style={styles.groupContent}>
+                <View
+                  style={[pal.border, styles.groupContent, styles.noTopBorder]}>
                   <FontAwesomeIcon
                     icon="ticket"
-                    style={styles.groupContentIcon}
+                    style={[pal.textLight, styles.groupContentIcon]}
                   />
                   <TextInput
-                    style={[styles.textInput]}
+                    style={[pal.text, styles.textInput]}
                     placeholder="Invite code"
-                    placeholderTextColor={colors.blue0}
+                    placeholderTextColor={pal.colors.textLight}
                     autoCapitalize="none"
                     autoCorrect={false}
                     autoFocus
@@ -233,16 +189,16 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
                   />
                 </View>
               ) : undefined}
-              <View style={styles.groupContent}>
+              <View style={[pal.border, styles.groupContent]}>
                 <FontAwesomeIcon
                   icon="envelope"
-                  style={styles.groupContentIcon}
+                  style={[pal.textLight, styles.groupContentIcon]}
                 />
                 <TextInput
                   testID="registerEmailInput"
-                  style={[styles.textInput]}
+                  style={[pal.text, styles.textInput]}
                   placeholder="Email address"
-                  placeholderTextColor={colors.blue0}
+                  placeholderTextColor={pal.colors.textLight}
                   autoCapitalize="none"
                   autoCorrect={false}
                   value={email}
@@ -250,13 +206,16 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
                   editable={!isProcessing}
                 />
               </View>
-              <View style={styles.groupContent}>
-                <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
+              <View style={[pal.border, styles.groupContent]}>
+                <FontAwesomeIcon
+                  icon="lock"
+                  style={[pal.textLight, styles.groupContentIcon]}
+                />
                 <TextInput
                   testID="registerPasswordInput"
-                  style={[styles.textInput]}
+                  style={[pal.text, styles.textInput]}
                   placeholder="Choose your password"
-                  placeholderTextColor={colors.blue0}
+                  placeholderTextColor={pal.colors.textLight}
                   autoCapitalize="none"
                   autoCorrect={false}
                   secureTextEntry
@@ -265,24 +224,28 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
                   editable={!isProcessing}
                 />
               </View>
-            </>
-          ) : undefined}
-        </View>
+            </View>
+          </>
+        ) : undefined}
         {serviceDescription ? (
           <>
-            <View style={styles.group}>
-              <View style={styles.groupTitle}>
-                <Text style={[s.white, s.f18, s.bold]}>
-                  Choose your username
-                </Text>
-              </View>
-              <View style={styles.groupContent}>
-                <FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
+            <View style={styles.groupLabel}>
+              <Text type="sm-bold" style={pal.text}>
+                Choose your username
+              </Text>
+            </View>
+            <View style={[pal.border, styles.group]}>
+              <View
+                style={[pal.border, styles.groupContent, styles.noTopBorder]}>
+                <FontAwesomeIcon
+                  icon="at"
+                  style={[pal.textLight, styles.groupContentIcon]}
+                />
                 <TextInput
                   testID="registerHandleInput"
-                  style={[styles.textInput]}
+                  style={[pal.text, styles.textInput]}
                   placeholder="eg alice"
-                  placeholderTextColor={colors.blue0}
+                  placeholderTextColor={pal.colors.textLight}
                   autoCapitalize="none"
                   value={handle}
                   onChangeText={v => setHandle(makeValidHandle(v))}
@@ -290,15 +253,15 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
                 />
               </View>
               {serviceDescription.availableUserDomains.length > 1 && (
-                <View style={styles.groupContent}>
+                <View style={[pal.border, styles.groupContent]}>
                   <FontAwesomeIcon
                     icon="globe"
                     style={styles.groupContentIcon}
                   />
                   <Picker
-                    style={styles.picker}
+                    style={[pal.text, styles.picker]}
                     labelStyle={styles.pickerLabel}
-                    iconStyle={styles.pickerIcon}
+                    iconStyle={pal.textLight}
                     value={userDomain}
                     items={serviceDescription.availableUserDomains.map(d => ({
                       label: `.${d}`,
@@ -309,41 +272,50 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
                   />
                 </View>
               )}
-              <View style={styles.groupContent}>
-                <Text style={[s.white, s.p10]}>
+              <View style={[pal.border, styles.groupContent]}>
+                <Text style={[pal.textLight, s.p10]}>
                   Your full username will be{' '}
-                  <Text style={[s.white, s.bold]}>
+                  <Text type="md-bold" style={pal.textLight}>
                     @{createFullHandle(handle, userDomain)}
                   </Text>
                 </Text>
               </View>
             </View>
-            <View style={[styles.group]}>
-              <View style={styles.groupTitle}>
-                <Text style={[s.white, s.f18, s.bold]}>Legal</Text>
-              </View>
-              <View style={styles.groupContent}>
+            <View style={styles.groupLabel}>
+              <Text type="sm-bold" style={pal.text}>
+                Legal
+              </Text>
+            </View>
+            <View style={[pal.border, styles.group]}>
+              <View
+                style={[pal.border, styles.groupContent, styles.noTopBorder]}>
                 <TouchableOpacity
                   testID="registerIs13Input"
                   style={styles.textBtn}
                   onPress={() => setIs13(!is13)}>
-                  <View style={is13 ? styles.checkboxFilled : styles.checkbox}>
+                  <View
+                    style={[
+                      pal.border,
+                      is13 ? styles.checkboxFilled : styles.checkbox,
+                    ]}>
                     {is13 && (
                       <FontAwesomeIcon icon="check" style={s.blue3} size={14} />
                     )}
                   </View>
-                  <Text style={[styles.textBtnLabel, s.f16]}>
+                  <Text style={[pal.text, styles.textBtnLabel]}>
                     I am 13 years old or older
                   </Text>
                 </TouchableOpacity>
               </View>
             </View>
-            <Policies />
+            <Policies serviceDescription={serviceDescription} />
           </>
         ) : undefined}
         <View style={[s.flexRow, s.pl20, s.pr20]}>
           <TouchableOpacity onPress={onPressBack}>
-            <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
+            <Text type="xl" style={pal.link}>
+              Back
+            </Text>
           </TouchableOpacity>
           <View style={s.flex1} />
           {isReady ? (
@@ -351,21 +323,27 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
               testID="createAccountButton"
               onPress={onPressNext}>
               {isProcessing ? (
-                <ActivityIndicator color="#fff" />
+                <ActivityIndicator />
               ) : (
-                <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
+                <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                  Next
+                </Text>
               )}
             </TouchableOpacity>
           ) : !serviceDescription && error ? (
             <TouchableOpacity
               testID="registerRetryButton"
               onPress={onPressRetryConnect}>
-              <Text style={[s.white, s.f18, s.bold, s.pr5]}>Retry</Text>
+              <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                Retry
+              </Text>
             </TouchableOpacity>
           ) : !serviceDescription ? (
             <>
               <ActivityIndicator color="#fff" />
-              <Text style={[s.white, s.f18, s.pl5, s.pr5]}>Connecting...</Text>
+              <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                Connecting...
+              </Text>
             </>
           ) : undefined}
         </View>
@@ -375,6 +353,69 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
   )
 }
 
+const Policies = ({
+  serviceDescription,
+}: {
+  serviceDescription: ServiceDescription
+}) => {
+  const pal = usePalette('default')
+  if (!serviceDescription) {
+    return <View />
+  }
+  const tos = validWebLink(serviceDescription.links?.termsOfService)
+  const pp = validWebLink(serviceDescription.links?.privacyPolicy)
+  if (!tos && !pp) {
+    return (
+      <View style={styles.policies}>
+        <View style={[styles.errorIcon, {borderColor: pal.colors.text}, s.mt2]}>
+          <FontAwesomeIcon icon="exclamation" style={pal.textLight} size={10} />
+        </View>
+        <Text style={[pal.textLight, s.pl5, s.flex1]}>
+          This service has not provided terms of service or a privacy policy.
+        </Text>
+      </View>
+    )
+  }
+  const els = []
+  if (tos) {
+    els.push(
+      <TextLink
+        key="tos"
+        href={tos}
+        text="Terms of Service"
+        style={[pal.link, s.underline]}
+      />,
+    )
+  }
+  if (pp) {
+    els.push(
+      <TextLink
+        key="pp"
+        href={pp}
+        text="Privacy Policy"
+        style={[pal.link, s.underline]}
+      />,
+    )
+  }
+  if (els.length === 2) {
+    els.splice(
+      1,
+      0,
+      <Text key="and" style={pal.textLight}>
+        {' '}
+        and{' '}
+      </Text>,
+    )
+  }
+  return (
+    <View style={styles.policies}>
+      <Text style={pal.textLight}>
+        By creating an account you agree to the {els}.
+      </Text>
+    </View>
+  )
+}
+
 function validWebLink(url?: string): string | undefined {
   return url && (url.startsWith('http://') || url.startsWith('https://'))
     ? url
@@ -382,42 +423,39 @@ function validWebLink(url?: string): string | undefined {
 }
 
 const styles = StyleSheet.create({
+  noTopBorder: {
+    borderTopWidth: 0,
+  },
   logoHero: {
     paddingTop: 30,
     paddingBottom: 40,
   },
   group: {
     borderWidth: 1,
-    borderColor: colors.white,
     borderRadius: 10,
     marginBottom: 20,
     marginHorizontal: 20,
-    backgroundColor: colors.blue3,
   },
-  groupTitle: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 8,
-    paddingHorizontal: 12,
+  groupLabel: {
+    paddingHorizontal: 20,
+    paddingBottom: 5,
   },
   groupContent: {
     borderTopWidth: 1,
-    borderTopColor: colors.blue1,
     flexDirection: 'row',
     alignItems: 'center',
   },
   groupContentIcon: {
-    color: 'white',
     marginLeft: 10,
   },
   textInput: {
     flex: 1,
     width: '100%',
-    backgroundColor: colors.blue3,
-    color: colors.white,
     paddingVertical: 10,
     paddingHorizontal: 12,
-    fontSize: 18,
+    fontSize: 17,
+    letterSpacing: 0.25,
+    fontWeight: '400',
     borderRadius: 10,
   },
   textBtn: {
@@ -427,47 +465,33 @@ const styles = StyleSheet.create({
   },
   textBtnLabel: {
     flex: 1,
-    color: colors.white,
     paddingVertical: 10,
     paddingHorizontal: 12,
-    fontSize: 18,
   },
   textBtnFakeInnerBtn: {
     flexDirection: 'row',
     alignItems: 'center',
-    backgroundColor: colors.blue2,
     borderRadius: 6,
     paddingVertical: 6,
     paddingHorizontal: 8,
     marginHorizontal: 6,
   },
   textBtnFakeInnerBtnIcon: {
-    color: colors.white,
     marginRight: 4,
   },
-  textBtnFakeInnerBtnLabel: {
-    color: colors.white,
-  },
   picker: {
     flex: 1,
     width: '100%',
-    backgroundColor: colors.blue3,
-    color: colors.white,
     paddingVertical: 10,
     paddingHorizontal: 12,
-    fontSize: 18,
+    fontSize: 17,
     borderRadius: 10,
   },
   pickerLabel: {
-    color: colors.white,
-    fontSize: 18,
-  },
-  pickerIcon: {
-    color: colors.white,
+    fontSize: 17,
   },
   checkbox: {
     borderWidth: 1,
-    borderColor: colors.white,
     borderRadius: 2,
     width: 16,
     height: 16,
@@ -475,8 +499,6 @@ const styles = StyleSheet.create({
   },
   checkboxFilled: {
     borderWidth: 1,
-    borderColor: colors.white,
-    backgroundColor: colors.white,
     borderRadius: 2,
     width: 16,
     height: 16,
@@ -489,8 +511,6 @@ const styles = StyleSheet.create({
     paddingBottom: 20,
   },
   error: {
-    borderWidth: 1,
-    borderColor: colors.red5,
     backgroundColor: colors.red4,
     flexDirection: 'row',
     alignItems: 'center',
@@ -509,7 +529,6 @@ const styles = StyleSheet.create({
   errorIcon: {
     borderWidth: 1,
     borderColor: colors.white,
-    color: colors.white,
     borderRadius: 30,
     width: 16,
     height: 16,
diff --git a/src/view/com/login/Logo.tsx b/src/view/com/login/Logo.tsx
index d1dc9c671..7045e4152 100644
--- a/src/view/com/login/Logo.tsx
+++ b/src/view/com/login/Logo.tsx
@@ -1,26 +1,29 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
 import Svg, {Circle, Line, Text as SvgText} from 'react-native-svg'
+import {s, gradients} from '../../lib/styles'
+import {Text} from '../util/text/Text'
 
-export const Logo = () => {
+export const Logo = ({color, size = 100}: {color: string; size?: number}) => {
   return (
     <View style={styles.logo}>
-      <Svg width="100" height="100">
+      <Svg width={size} height={size} viewBox="0 0 100 100">
         <Circle
           cx="50"
           cy="50"
           r="46"
           fill="none"
-          stroke="white"
+          stroke={color}
           strokeWidth={2}
         />
-        <Line stroke="white" strokeWidth={1} x1="30" x2="30" y1="0" y2="100" />
-        <Line stroke="white" strokeWidth={1} x1="74" x2="74" y1="0" y2="100" />
-        <Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="22" y2="22" />
-        <Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="74" y2="74" />
+        <Line stroke={color} strokeWidth={1} x1="30" x2="30" y1="0" y2="100" />
+        <Line stroke={color} strokeWidth={1} x1="74" x2="74" y1="0" y2="100" />
+        <Line stroke={color} strokeWidth={1} x1="0" x2="100" y1="22" y2="22" />
+        <Line stroke={color} strokeWidth={1} x1="0" x2="100" y1="74" y2="74" />
         <SvgText
           fill="none"
-          stroke="white"
+          stroke={color}
           strokeWidth={2}
           fontSize="60"
           fontWeight="bold"
@@ -34,9 +37,32 @@ export const Logo = () => {
   )
 }
 
+export const LogoTextHero = () => {
+  return (
+    <LinearGradient
+      colors={[gradients.blue.start, gradients.blue.end]}
+      start={{x: 0, y: 0}}
+      end={{x: 1, y: 1}}
+      style={[styles.textHero]}>
+      <Logo color="white" size={40} />
+      <Text type="title-lg" style={[s.white, s.pl10]}>
+        Bluesky
+      </Text>
+    </LinearGradient>
+  )
+}
+
 const styles = StyleSheet.create({
   logo: {
     flexDirection: 'row',
     justifyContent: 'center',
   },
+  textHero: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingRight: 20,
+    paddingVertical: 15,
+    marginBottom: 20,
+  },
 })
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
index e99aaa651..a39ea5e74 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/login/Signin.tsx
@@ -11,23 +11,28 @@ import {
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as EmailValidator from 'email-validator'
 import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
-import {Logo} from './Logo'
+import {LogoTextHero} from './Logo'
 import {Text} from '../util/text/Text'
+import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
 import {createFullHandle, toNiceDomain} from '../../../lib/strings'
 import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state'
 import {ServiceDescription} from '../../../state/models/session'
 import {ServerInputModal} from '../../../state/models/shell-ui'
+import {AccountData} from '../../../state/models/session'
 import {isNetworkError} from '../../../lib/errors'
+import {usePalette} from '../../lib/hooks/usePalette'
 
 enum Forms {
   Login,
+  ChooseAccount,
   ForgotPassword,
   SetNewPassword,
   PasswordUpdated,
 }
 
 export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
+  const pal = usePalette('default')
   const store = useStores()
   const [error, setError] = useState<string>('')
   const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
@@ -35,7 +40,18 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
   const [serviceDescription, setServiceDescription] = useState<
     ServiceDescription | undefined
   >(undefined)
-  const [currentForm, setCurrentForm] = useState<Forms>(Forms.Login)
+  const [initialHandle, setInitialHandle] = useState<string>('')
+  const [currentForm, setCurrentForm] = useState<Forms>(
+    store.session.hasAccounts ? Forms.ChooseAccount : Forms.Login,
+  )
+
+  const onSelectAccount = (account?: AccountData) => {
+    if (account?.service) {
+      setServiceUrl(account.service)
+    }
+    setInitialHandle(account?.handle || '')
+    setCurrentForm(Forms.Login)
+  }
 
   const gotoForm = (form: Forms) => () => {
     setError('')
@@ -73,16 +89,14 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
   const onPressRetryConnect = () => setRetryDescribeTrigger({})
 
   return (
-    <KeyboardAvoidingView testID="signIn" behavior="padding" style={{flex: 1}}>
-      <View style={styles.logoHero}>
-        <Logo />
-      </View>
+    <KeyboardAvoidingView testID="signIn" behavior="padding" style={[pal.view]}>
       {currentForm === Forms.Login ? (
         <LoginForm
           store={store}
           error={error}
           serviceUrl={serviceUrl}
           serviceDescription={serviceDescription}
+          initialHandle={initialHandle}
           setError={setError}
           setServiceUrl={setServiceUrl}
           onPressBack={onPressBack}
@@ -90,6 +104,13 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
           onPressRetryConnect={onPressRetryConnect}
         />
       ) : undefined}
+      {currentForm === Forms.ChooseAccount ? (
+        <ChooseAccountForm
+          store={store}
+          onSelectAccount={onSelectAccount}
+          onPressBack={onPressBack}
+        />
+      ) : undefined}
       {currentForm === Forms.ForgotPassword ? (
         <ForgotPasswordForm
           store={store}
@@ -119,11 +140,109 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
   )
 }
 
+const ChooseAccountForm = ({
+  store,
+  onSelectAccount,
+  onPressBack,
+}: {
+  store: RootStoreModel
+  onSelectAccount: (account?: AccountData) => void
+  onPressBack: () => void
+}) => {
+  const pal = usePalette('default')
+  const [isProcessing, setIsProcessing] = React.useState(false)
+
+  const onTryAccount = async (account: AccountData) => {
+    if (account.accessJwt && account.refreshJwt) {
+      setIsProcessing(true)
+      if (await store.session.resumeSession(account)) {
+        setIsProcessing(false)
+        return
+      }
+      setIsProcessing(false)
+    }
+    onSelectAccount(account)
+  }
+
+  return (
+    <View testID="chooseAccountForm">
+      <LogoTextHero />
+      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
+        Sign in as...
+      </Text>
+      {store.session.accounts.map(account => (
+        <TouchableOpacity
+          testID={`chooseAccountBtn-${account.handle}`}
+          key={account.did}
+          style={[pal.borderDark, styles.group, s.mb5]}
+          onPress={() => onTryAccount(account)}>
+          <View
+            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
+            <View style={s.p10}>
+              <UserAvatar
+                displayName={account.displayName}
+                handle={account.handle}
+                avatar={account.aviUrl}
+                size={30}
+              />
+            </View>
+            <Text style={styles.accountText}>
+              <Text type="lg-bold" style={pal.text}>
+                {account.displayName || account.handle}{' '}
+              </Text>
+              <Text type="lg" style={[pal.textLight]}>
+                {account.handle}
+              </Text>
+            </Text>
+            <FontAwesomeIcon
+              icon="angle-right"
+              size={16}
+              style={[pal.text, s.mr10]}
+            />
+          </View>
+        </TouchableOpacity>
+      ))}
+      <TouchableOpacity
+        testID="chooseNewAccountBtn"
+        style={[pal.borderDark, styles.group]}
+        onPress={() => onSelectAccount(undefined)}>
+        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
+          <View style={s.p10}>
+            <View
+              style={[pal.btn, {width: 30, height: 30, borderRadius: 15}]}
+            />
+          </View>
+          <Text style={styles.accountText}>
+            <Text type="lg" style={pal.text}>
+              Other account
+            </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}>
+          <Text type="xl" style={[pal.link, s.pl5]}>
+            Back
+          </Text>
+        </TouchableOpacity>
+        <View style={s.flex1} />
+        {isProcessing && <ActivityIndicator />}
+      </View>
+    </View>
+  )
+}
+
 const LoginForm = ({
   store,
   error,
   serviceUrl,
   serviceDescription,
+  initialHandle,
   setError,
   setServiceUrl,
   onPressRetryConnect,
@@ -134,14 +253,16 @@ const LoginForm = ({
   error: string
   serviceUrl: string
   serviceDescription: ServiceDescription | undefined
+  initialHandle: string
   setError: (v: string) => void
   setServiceUrl: (v: string) => void
   onPressRetryConnect: () => void
   onPressBack: () => void
   onPressForgotPassword: () => void
 }) => {
+  const pal = usePalette('default')
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [handle, setHandle] = useState<string>('')
+  const [handle, setHandle] = useState<string>(initialHandle)
   const [password, setPassword] = useState<string>('')
 
   const onPressSelectService = () => {
@@ -197,31 +318,44 @@ const LoginForm = ({
 
   const isReady = !!serviceDescription && !!handle && !!password
   return (
-    <>
-      <View testID="loginFormView" style={styles.group}>
-        <TouchableOpacity
-          testID="loginSelectServiceButton"
-          style={[styles.groupTitle, {paddingRight: 0, paddingVertical: 6}]}
-          onPress={onPressSelectService}>
-          <Text style={[s.flex1, s.white, s.f18, s.bold]} numberOfLines={1}>
-            Sign in to {toNiceDomain(serviceUrl)}
-          </Text>
-          <View style={styles.textBtnFakeInnerBtn}>
-            <FontAwesomeIcon
-              icon="pen"
-              size={12}
-              style={styles.textBtnFakeInnerBtnIcon}
-            />
-            <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
-          </View>
-        </TouchableOpacity>
-        <View style={styles.groupContent}>
-          <FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
+    <View testID="loginForm">
+      <LogoTextHero />
+      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
+        Sign into
+      </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}>
+            <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} />
+            </View>
+          </TouchableOpacity>
+        </View>
+      </View>
+      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
+        Account
+      </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={styles.textInput}
+            style={[pal.text, styles.textInput]}
             placeholder="Username"
-            placeholderTextColor={colors.blue0}
+            placeholderTextColor={pal.colors.textLight}
             autoCapitalize="none"
             autoFocus
             autoCorrect={false}
@@ -230,13 +364,16 @@ const LoginForm = ({
             editable={!isProcessing}
           />
         </View>
-        <View style={styles.groupContent}>
-          <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
+        <View style={[pal.borderDark, styles.groupContent]}>
+          <FontAwesomeIcon
+            icon="lock"
+            style={[pal.textLight, styles.groupContentIcon]}
+          />
           <TextInput
             testID="loginPasswordInput"
-            style={styles.textInput}
+            style={[pal.text, styles.textInput]}
             placeholder="Password"
-            placeholderTextColor={colors.blue0}
+            placeholderTextColor={pal.colors.textLight}
             autoCapitalize="none"
             autoCorrect={false}
             secureTextEntry
@@ -248,7 +385,7 @@ const LoginForm = ({
             testID="forgotPasswordButton"
             style={styles.textInputInnerBtn}
             onPress={onPressForgotPassword}>
-            <Text style={styles.textInputInnerBtnLabel}>Forgot</Text>
+            <Text style={pal.link}>Forgot</Text>
           </TouchableOpacity>
         </View>
       </View>
@@ -264,29 +401,37 @@ const LoginForm = ({
       ) : undefined}
       <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
         <TouchableOpacity onPress={onPressBack}>
-          <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
+          <Text type="xl" style={[pal.link, s.pl5]}>
+            Back
+          </Text>
         </TouchableOpacity>
         <View style={s.flex1} />
         {!serviceDescription && error ? (
           <TouchableOpacity
             testID="loginRetryButton"
             onPress={onPressRetryConnect}>
-            <Text style={[s.white, s.f18, s.bold, s.pr5]}>Retry</Text>
+            <Text type="xl-bold" style={[pal.link, s.pr5]}>
+              Retry
+            </Text>
           </TouchableOpacity>
         ) : !serviceDescription ? (
           <>
-            <ActivityIndicator color="#fff" />
-            <Text style={[s.white, s.f18, s.pl10]}>Connecting...</Text>
+            <ActivityIndicator />
+            <Text type="xl" style={[pal.textLight, s.pl10]}>
+              Connecting...
+            </Text>
           </>
         ) : isProcessing ? (
-          <ActivityIndicator color="#fff" />
+          <ActivityIndicator />
         ) : isReady ? (
           <TouchableOpacity testID="loginNextButton" onPress={onPressNext}>
-            <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
+            <Text type="xl-bold" style={[pal.link, s.pr5]}>
+              Next
+            </Text>
           </TouchableOpacity>
         ) : undefined}
       </View>
-    </>
+    </View>
   )
 }
 
@@ -309,6 +454,7 @@ const ForgotPasswordForm = ({
   onPressBack: () => void
   onEmailSent: () => void
 }) => {
+  const pal = usePalette('default')
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [email, setEmail] = useState<string>('')
 
@@ -344,72 +490,88 @@ const ForgotPasswordForm = ({
 
   return (
     <>
-      <Text style={styles.screenTitle}>Reset password</Text>
-      <Text style={styles.instructions}>
-        Enter the email you used to create your account. We'll send you a "reset
-        code" so you can set a new password.
-      </Text>
-      <View testID="forgotPasswordView" style={styles.group}>
-        <TouchableOpacity
-          testID="forgotPasswordSelectServiceButton"
-          style={[styles.groupContent, {borderTopWidth: 0}]}
-          onPress={onPressSelectService}>
-          <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
-          <Text style={styles.textInput} numberOfLines={1}>
-            {toNiceDomain(serviceUrl)}
-          </Text>
-          <View style={styles.textBtnFakeInnerBtn}>
+      <LogoTextHero />
+      <View>
+        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
+          Reset password
+        </Text>
+        <Text type="md" style={[pal.text, styles.instructions]}>
+          Enter the email you used to create your account. We'll send you a
+          "reset code" so you can set a new password.
+        </Text>
+        <View
+          testID="forgotPasswordView"
+          style={[pal.borderDark, pal.view, styles.group]}>
+          <TouchableOpacity
+            testID="forgotPasswordSelectServiceButton"
+            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
+            onPress={onPressSelectService}>
+            <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} />
+            </View>
+          </TouchableOpacity>
+          <View style={[pal.borderDark, styles.groupContent]}>
             <FontAwesomeIcon
-              icon="pen"
-              size={12}
-              style={styles.textBtnFakeInnerBtnIcon}
+              icon="envelope"
+              style={[pal.textLight, styles.groupContentIcon]}
+            />
+            <TextInput
+              testID="forgotPasswordEmail"
+              style={[pal.text, styles.textInput]}
+              placeholder="Email address"
+              placeholderTextColor={pal.colors.textLight}
+              autoCapitalize="none"
+              autoFocus
+              autoCorrect={false}
+              value={email}
+              onChangeText={setEmail}
+              editable={!isProcessing}
             />
-            <Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
           </View>
-        </TouchableOpacity>
-        <View style={styles.groupContent}>
-          <FontAwesomeIcon icon="envelope" style={styles.groupContentIcon} />
-          <TextInput
-            testID="forgotPasswordEmail"
-            style={styles.textInput}
-            placeholder="Email address"
-            placeholderTextColor={colors.blue0}
-            autoCapitalize="none"
-            autoFocus
-            autoCorrect={false}
-            value={email}
-            onChangeText={setEmail}
-            editable={!isProcessing}
-          />
         </View>
-      </View>
-      {error ? (
-        <View style={styles.error}>
-          <View style={styles.errorIcon}>
-            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-          </View>
-          <View style={s.flex1}>
-            <Text style={[s.white, s.bold]}>{error}</Text>
+        {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>
-        </View>
-      ) : undefined}
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <TouchableOpacity onPress={onPressBack}>
-          <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-        {!serviceDescription || isProcessing ? (
-          <ActivityIndicator color="#fff" />
-        ) : !email ? (
-          <Text style={[s.blue1, s.f18, s.bold, s.pr5]}>Next</Text>
-        ) : (
-          <TouchableOpacity testID="newPasswordButton" onPress={onPressNext}>
-            <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
-          </TouchableOpacity>
-        )}
-        {!serviceDescription || isProcessing ? (
-          <Text style={[s.white, s.f18, s.pl10]}>Processing...</Text>
         ) : undefined}
+        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
+          <TouchableOpacity onPress={onPressBack}>
+            <Text type="xl" style={[pal.link, s.pl5]}>
+              Back
+            </Text>
+          </TouchableOpacity>
+          <View style={s.flex1} />
+          {!serviceDescription || isProcessing ? (
+            <ActivityIndicator />
+          ) : !email ? (
+            <Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}>
+              Next
+            </Text>
+          ) : (
+            <TouchableOpacity testID="newPasswordButton" onPress={onPressNext}>
+              <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                Next
+              </Text>
+            </TouchableOpacity>
+          )}
+          {!serviceDescription || isProcessing ? (
+            <Text type="xl" style={[pal.textLight, s.pl10]}>
+              Processing...
+            </Text>
+          ) : undefined}
+        </View>
       </View>
     </>
   )
@@ -430,6 +592,7 @@ const SetNewPasswordForm = ({
   onPressBack: () => void
   onPasswordSet: () => void
 }) => {
+  const pal = usePalette('default')
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [resetCode, setResetCode] = useState<string>('')
   const [password, setPassword] = useState<string>('')
@@ -458,87 +621,119 @@ const SetNewPasswordForm = ({
 
   return (
     <>
-      <Text style={styles.screenTitle}>Set new password</Text>
-      <Text style={styles.instructions}>
-        You will receive an email with a "reset code." Enter that code here,
-        then enter your new password.
-      </Text>
-      <View testID="newPasswordView" style={styles.group}>
-        <View style={[styles.groupContent, {borderTopWidth: 0}]}>
-          <FontAwesomeIcon icon="ticket" style={styles.groupContentIcon} />
-          <TextInput
-            testID="resetCodeInput"
-            style={[styles.textInput]}
-            placeholder="Reset code"
-            placeholderTextColor={colors.blue0}
-            autoCapitalize="none"
-            autoCorrect={false}
-            autoFocus
-            value={resetCode}
-            onChangeText={setResetCode}
-            editable={!isProcessing}
-          />
-        </View>
-        <View style={styles.groupContent}>
-          <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
-          <TextInput
-            testID="newPasswordInput"
-            style={styles.textInput}
-            placeholder="New password"
-            placeholderTextColor={colors.blue0}
-            autoCapitalize="none"
-            autoCorrect={false}
-            secureTextEntry
-            value={password}
-            onChangeText={setPassword}
-            editable={!isProcessing}
-          />
-        </View>
-      </View>
-      {error ? (
-        <View style={styles.error}>
-          <View style={styles.errorIcon}>
-            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
+      <LogoTextHero />
+      <View>
+        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
+          Set new password
+        </Text>
+        <Text type="lg" style={[pal.text, styles.instructions]}>
+          You will receive an email with a "reset code." Enter that code here,
+          then enter your new password.
+        </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="Reset code"
+              placeholderTextColor={pal.colors.textLight}
+              autoCapitalize="none"
+              autoCorrect={false}
+              autoFocus
+              value={resetCode}
+              onChangeText={setResetCode}
+              editable={!isProcessing}
+            />
           </View>
-          <View style={s.flex1}>
-            <Text style={[s.white, s.bold]}>{error}</Text>
+          <View style={[pal.borderDark, styles.groupContent]}>
+            <FontAwesomeIcon
+              icon="lock"
+              style={[pal.textLight, styles.groupContentIcon]}
+            />
+            <TextInput
+              testID="newPasswordInput"
+              style={[pal.text, styles.textInput]}
+              placeholder="New password"
+              placeholderTextColor={pal.colors.textLight}
+              autoCapitalize="none"
+              autoCorrect={false}
+              secureTextEntry
+              value={password}
+              onChangeText={setPassword}
+              editable={!isProcessing}
+            />
           </View>
         </View>
-      ) : undefined}
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <TouchableOpacity onPress={onPressBack}>
-          <Text style={[s.white, s.f18, s.pl5]}>Back</Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-        {isProcessing ? (
-          <ActivityIndicator color="#fff" />
-        ) : !resetCode || !password ? (
-          <Text style={[s.blue1, s.f18, s.bold, s.pr5]}>Next</Text>
-        ) : (
-          <TouchableOpacity testID="setNewPasswordButton" onPress={onPressNext}>
-            <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
-          </TouchableOpacity>
-        )}
-        {isProcessing ? (
-          <Text style={[s.white, s.f18, s.pl10]}>Updating...</Text>
+        {error ? (
+          <View style={styles.error}>
+            <View style={styles.errorIcon}>
+              <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
+            </View>
+            <View style={s.flex1}>
+              <Text style={[s.white, s.bold]}>{error}</Text>
+            </View>
+          </View>
         ) : undefined}
+        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
+          <TouchableOpacity onPress={onPressBack}>
+            <Text type="xl" style={[pal.link, s.pl5]}>
+              Back
+            </Text>
+          </TouchableOpacity>
+          <View style={s.flex1} />
+          {isProcessing ? (
+            <ActivityIndicator />
+          ) : !resetCode || !password ? (
+            <Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}>
+              Next
+            </Text>
+          ) : (
+            <TouchableOpacity
+              testID="setNewPasswordButton"
+              onPress={onPressNext}>
+              <Text type="xl-bold" style={[pal.link, s.pr5]}>
+                Next
+              </Text>
+            </TouchableOpacity>
+          )}
+          {isProcessing ? (
+            <Text type="xl" style={[pal.textLight, s.pl10]}>
+              Updating...
+            </Text>
+          ) : undefined}
+        </View>
       </View>
     </>
   )
 }
 
 const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
+  const pal = usePalette('default')
   return (
     <>
-      <Text style={styles.screenTitle}>Password updated!</Text>
-      <Text style={styles.instructions}>
-        You can now sign in with your new password.
-      </Text>
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <View style={s.flex1} />
-        <TouchableOpacity onPress={onPressNext}>
-          <Text style={[s.white, s.f18, s.bold, s.pr5]}>Okay</Text>
-        </TouchableOpacity>
+      <LogoTextHero />
+      <View>
+        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
+          Password updated!
+        </Text>
+        <Text type="lg" style={[pal.text, styles.instructions]}>
+          You can now sign in with your new password.
+        </Text>
+        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
+          <View style={s.flex1} />
+          <TouchableOpacity onPress={onPressNext}>
+            <Text type="xl-bold" style={[pal.link, s.pr5]}>
+              Okay
+            </Text>
+          </TouchableOpacity>
+        </View>
       </View>
     </>
   )
@@ -546,53 +741,42 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
 
 const styles = StyleSheet.create({
   screenTitle: {
-    color: colors.white,
-    fontSize: 26,
     marginBottom: 10,
     marginHorizontal: 20,
   },
   instructions: {
-    color: colors.white,
-    fontSize: 16,
     marginBottom: 20,
     marginHorizontal: 20,
   },
-  logoHero: {
-    paddingTop: 30,
-    paddingBottom: 40,
-  },
   group: {
     borderWidth: 1,
-    borderColor: colors.white,
     borderRadius: 10,
     marginBottom: 20,
     marginHorizontal: 20,
-    backgroundColor: colors.blue3,
   },
-  groupTitle: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 8,
-    paddingHorizontal: 12,
+  groupLabel: {
+    paddingHorizontal: 20,
+    paddingBottom: 5,
   },
   groupContent: {
     borderTopWidth: 1,
-    borderTopColor: colors.blue1,
     flexDirection: 'row',
     alignItems: 'center',
   },
+  noTopBorder: {
+    borderTopWidth: 0,
+  },
   groupContentIcon: {
-    color: 'white',
     marginLeft: 10,
   },
   textInput: {
     flex: 1,
     width: '100%',
-    backgroundColor: colors.blue3,
-    color: colors.white,
     paddingVertical: 10,
     paddingHorizontal: 12,
-    fontSize: 18,
+    fontSize: 17,
+    letterSpacing: 0.25,
+    fontWeight: '400',
     borderRadius: 10,
   },
   textInputInnerBtn: {
@@ -602,28 +786,31 @@ const styles = StyleSheet.create({
     paddingHorizontal: 8,
     marginHorizontal: 6,
   },
-  textInputInnerBtnLabel: {
-    color: colors.white,
+  textBtn: {
+    flexDirection: 'row',
+    flex: 1,
+    alignItems: 'center',
+  },
+  textBtnLabel: {
+    flex: 1,
+    paddingVertical: 10,
+    paddingHorizontal: 12,
   },
   textBtnFakeInnerBtn: {
     flexDirection: 'row',
     alignItems: 'center',
-    backgroundColor: colors.blue2,
     borderRadius: 6,
     paddingVertical: 6,
     paddingHorizontal: 8,
     marginHorizontal: 6,
   },
-  textBtnFakeInnerBtnIcon: {
-    color: colors.white,
-    marginRight: 4,
-  },
-  textBtnFakeInnerBtnLabel: {
-    color: colors.white,
+  accountText: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'baseline',
+    paddingVertical: 10,
   },
   error: {
-    borderWidth: 1,
-    borderColor: colors.red5,
     backgroundColor: colors.red4,
     flexDirection: 'row',
     alignItems: 'center',
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index 884fb91e6..c8174f3cd 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -33,7 +33,7 @@ export function Component({
   }
 
   return (
-    <View style={s.flex1}>
+    <View style={s.flex1} testID="serverInputModal">
       <Text style={[s.textCenter, s.bold, s.f18]}>Choose Service</Text>
       <BottomSheetScrollView style={styles.inner}>
         <View style={styles.group}>
@@ -64,6 +64,7 @@ export function Component({
           <Text style={styles.label}>Other service</Text>
           <View style={{flexDirection: 'row'}}>
             <BottomSheetTextInput
+              testID="customServerTextInput"
               style={styles.textInput}
               placeholder="e.g. https://bsky.app"
               placeholderTextColor={colors.gray4}
@@ -74,6 +75,7 @@ export function Component({
               onChangeText={setCustomUrl}
             />
             <TouchableOpacity
+              testID="customServerSelectBtn"
               style={styles.textInputBtn}
               onPress={() => doSelect(customUrl)}>
               <FontAwesomeIcon
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index d1e9b397b..761553cc5 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -49,6 +49,7 @@ export const ViewHeader = observer(function ViewHeader({
   return (
     <View style={[styles.header, pal.view]}>
       <TouchableOpacity
+        testID="viewHeaderBackOrMenuBtn"
         onPress={canGoBack ? onPressBack : onPressMenu}
         hitSlop={BACK_HITSLOP}
         style={canGoBack ? styles.backIcon : styles.backIconWide}>
diff --git a/src/view/lib/ThemeContext.tsx b/src/view/lib/ThemeContext.tsx
index 54ae71277..16a7d9cb3 100644
--- a/src/view/lib/ThemeContext.tsx
+++ b/src/view/lib/ThemeContext.tsx
@@ -18,6 +18,7 @@ export type PaletteColor = {
   textInverted: string
   link: string
   border: string
+  borderDark: string
   icon: string
   [k: string]: string
 }
diff --git a/src/view/lib/hooks/usePalette.ts b/src/view/lib/hooks/usePalette.ts
index 890439f34..5b9929c7d 100644
--- a/src/view/lib/hooks/usePalette.ts
+++ b/src/view/lib/hooks/usePalette.ts
@@ -6,6 +6,7 @@ export interface UsePaletteValue {
   view: ViewStyle
   btn: ViewStyle
   border: ViewStyle
+  borderDark: ViewStyle
   text: TextStyle
   textLight: TextStyle
   textInverted: TextStyle
@@ -25,6 +26,9 @@ export function usePalette(color: PaletteColorName): UsePaletteValue {
     border: {
       borderColor: palette.border,
     },
+    borderDark: {
+      borderColor: palette.borderDark,
+    },
     text: {
       color: palette.text,
     },
diff --git a/src/view/lib/themes.ts b/src/view/lib/themes.ts
index b9e2bdacf..84e2b7883 100644
--- a/src/view/lib/themes.ts
+++ b/src/view/lib/themes.ts
@@ -13,6 +13,7 @@ export const defaultTheme: Theme = {
       textInverted: colors.white,
       link: colors.blue3,
       border: '#f0e9e9',
+      borderDark: '#e0d9d9',
       icon: colors.gray3,
 
       // non-standard
@@ -32,6 +33,7 @@ export const defaultTheme: Theme = {
       textInverted: colors.blue3,
       link: colors.blue0,
       border: colors.blue4,
+      borderDark: colors.blue5,
       icon: colors.blue4,
     },
     secondary: {
@@ -42,6 +44,7 @@ export const defaultTheme: Theme = {
       textInverted: colors.green4,
       link: colors.green1,
       border: colors.green4,
+      borderDark: colors.green5,
       icon: colors.green4,
     },
     inverted: {
@@ -52,6 +55,7 @@ export const defaultTheme: Theme = {
       textInverted: colors.black,
       link: colors.blue2,
       border: colors.gray3,
+      borderDark: colors.gray2,
       icon: colors.gray5,
     },
     error: {
@@ -62,6 +66,7 @@ export const defaultTheme: Theme = {
       textInverted: colors.red3,
       link: colors.red1,
       border: colors.red4,
+      borderDark: colors.red5,
       icon: colors.red4,
     },
   },
@@ -257,6 +262,7 @@ export const darkTheme: Theme = {
       textInverted: colors.black,
       link: colors.blue3,
       border: colors.gray6,
+      borderDark: colors.gray5,
       icon: colors.gray5,
 
       // non-standard
@@ -284,6 +290,7 @@ export const darkTheme: Theme = {
       textInverted: colors.white,
       link: colors.blue3,
       border: colors.gray3,
+      borderDark: colors.gray4,
       icon: colors.gray1,
     },
   },
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
index 8363dbfe0..7d99f1444 100644
--- a/src/view/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -1,17 +1,21 @@
 import React, {useState} from 'react'
 import {
+  SafeAreaView,
   StyleSheet,
   TouchableOpacity,
   View,
   useWindowDimensions,
 } from 'react-native'
 import Svg, {Line} from 'react-native-svg'
+import LinearGradient from 'react-native-linear-gradient'
 import {observer} from 'mobx-react-lite'
 import {Signin} from '../com/login/Signin'
 import {Logo} from '../com/login/Logo'
 import {CreateAccount} from '../com/login/CreateAccount'
 import {Text} from '../com/util/text/Text'
+import {ErrorBoundary} from '../com/util/ErrorBoundary'
 import {s, colors} from '../lib/styles'
+import {usePalette} from '../lib/hooks/usePalette'
 
 enum ScreenState {
   SigninOrCreateAccount,
@@ -31,7 +35,7 @@ const SigninOrCreateAccount = ({
   return (
     <>
       <View style={styles.hero}>
-        <Logo />
+        <Logo color="white" />
         <Text style={styles.title}>Bluesky</Text>
         <Text style={styles.subtitle}>[ private beta ]</Text>
       </View>
@@ -76,40 +80,61 @@ const SigninOrCreateAccount = ({
 
 export const Login = observer(
   (/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
+    const pal = usePalette('default')
     const [screenState, setScreenState] = useState<ScreenState>(
       ScreenState.SigninOrCreateAccount,
     )
 
+    if (screenState === ScreenState.SigninOrCreateAccount) {
+      return (
+        <LinearGradient
+          colors={['#007CFF', '#00BCFF']}
+          start={{x: 0, y: 0.8}}
+          end={{x: 0, y: 1}}
+          style={styles.container}>
+          <SafeAreaView testID="noSessionView" style={styles.container}>
+            <ErrorBoundary>
+              <SigninOrCreateAccount
+                onPressSignin={() => setScreenState(ScreenState.Signin)}
+                onPressCreateAccount={() =>
+                  setScreenState(ScreenState.CreateAccount)
+                }
+              />
+            </ErrorBoundary>
+          </SafeAreaView>
+        </LinearGradient>
+      )
+    }
+
     return (
-      <View style={styles.outer}>
-        {screenState === ScreenState.SigninOrCreateAccount ? (
-          <SigninOrCreateAccount
-            onPressSignin={() => setScreenState(ScreenState.Signin)}
-            onPressCreateAccount={() =>
-              setScreenState(ScreenState.CreateAccount)
-            }
-          />
-        ) : undefined}
-        {screenState === ScreenState.Signin ? (
-          <Signin
-            onPressBack={() =>
-              setScreenState(ScreenState.SigninOrCreateAccount)
-            }
-          />
-        ) : undefined}
-        {screenState === ScreenState.CreateAccount ? (
-          <CreateAccount
-            onPressBack={() =>
-              setScreenState(ScreenState.SigninOrCreateAccount)
-            }
-          />
-        ) : undefined}
+      <View style={[styles.container, pal.view]}>
+        <SafeAreaView testID="noSessionView" style={styles.container}>
+          <ErrorBoundary>
+            {screenState === ScreenState.Signin ? (
+              <Signin
+                onPressBack={() =>
+                  setScreenState(ScreenState.SigninOrCreateAccount)
+                }
+              />
+            ) : undefined}
+            {screenState === ScreenState.CreateAccount ? (
+              <CreateAccount
+                onPressBack={() =>
+                  setScreenState(ScreenState.SigninOrCreateAccount)
+                }
+              />
+            ) : undefined}
+          </ErrorBoundary>
+        </SafeAreaView>
       </View>
     )
   },
 )
 
 const styles = StyleSheet.create({
+  container: {
+    height: '100%',
+  },
   outer: {
     flex: 1,
   },
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 22230f24c..2c6982685 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -1,5 +1,12 @@
 import React, {useEffect} from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {
+  ActivityIndicator,
+  ScrollView,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {observer} from 'mobx-react-lite'
 import {useStores} from '../../state'
 import {ScreenParams} from '../routes'
@@ -7,8 +14,10 @@ import {s} from '../lib/styles'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Link} from '../com/util/Link'
 import {Text} from '../com/util/text/Text'
+import * as Toast from '../com/util/Toast'
 import {UserAvatar} from '../com/util/UserAvatar'
 import {usePalette} from '../lib/hooks/usePalette'
+import {AccountData} from '../../state/models/session'
 
 export const Settings = observer(function Settings({
   navIdx,
@@ -16,6 +25,7 @@ export const Settings = observer(function Settings({
 }: ScreenParams) {
   const pal = usePalette('default')
   const store = useStores()
+  const [isSwitching, setIsSwitching] = React.useState(false)
 
   useEffect(() => {
     if (!visible) {
@@ -25,45 +35,114 @@ export const Settings = observer(function Settings({
     store.nav.setTitle(navIdx, 'Settings')
   }, [visible, store])
 
+  const onPressSwitchAccount = async (acct: AccountData) => {
+    setIsSwitching(true)
+    if (await store.session.resumeSession(acct)) {
+      setIsSwitching(false)
+      Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
+      return
+    }
+    setIsSwitching(false)
+    Toast.show('Sorry! We need you to enter your password.')
+    store.session.clear()
+  }
+  const onPressAddAccount = () => {
+    store.session.clear()
+  }
   const onPressSignout = () => {
     store.session.logout()
   }
 
   return (
-    <View style={[s.flex1]}>
+    <View style={[s.h100pct]} testID="settingsScreen">
       <ViewHeader title="Settings" />
-      <View style={[s.mt10, s.pl10, s.pr10, s.flex1]}>
+      <ScrollView style={[s.mt10, s.pl10, s.pr10, s.h100pct]}>
         <View style={[s.flexRow]}>
-          <Text type="xl" style={pal.text}>
+          <Text type="xl-bold" style={pal.text}>
             Signed in as
           </Text>
           <View style={s.flex1} />
-          <TouchableOpacity onPress={onPressSignout}>
+          <TouchableOpacity
+            testID="signOutBtn"
+            onPress={isSwitching ? undefined : onPressSignout}>
             <Text type="xl-medium" style={pal.link}>
               Sign out
             </Text>
           </TouchableOpacity>
         </View>
-        <Link
-          href={`/profile/${store.me.handle}`}
-          title="Your profile"
-          noFeedback>
+        {isSwitching ? (
           <View style={[pal.view, styles.profile]}>
+            <ActivityIndicator />
+          </View>
+        ) : (
+          <Link
+            href={`/profile/${store.me.handle}`}
+            title="Your profile"
+            noFeedback>
+            <View style={[pal.view, styles.profile]}>
+              <UserAvatar
+                size={40}
+                displayName={store.me.displayName}
+                handle={store.me.handle || ''}
+                avatar={store.me.avatar}
+              />
+              <View style={[s.ml10]}>
+                <Text type="xl-bold" style={pal.text}>
+                  {store.me.displayName || store.me.handle}
+                </Text>
+                <Text style={pal.textLight}>@{store.me.handle}</Text>
+              </View>
+            </View>
+          </Link>
+        )}
+        <Text type="sm-medium" style={pal.text}>
+          Switch to:
+        </Text>
+        {store.session.switchableAccounts.map(account => (
+          <TouchableOpacity
+            testID={`switchToAccountBtn-${account.handle}`}
+            key={account.did}
+            style={[
+              pal.view,
+              styles.profile,
+              s.mb2,
+              isSwitching && styles.dimmed,
+            ]}
+            onPress={
+              isSwitching ? undefined : () => onPressSwitchAccount(account)
+            }>
             <UserAvatar
               size={40}
-              displayName={store.me.displayName}
-              handle={store.me.handle || ''}
-              avatar={store.me.avatar}
+              displayName={account.displayName}
+              handle={account.handle || ''}
+              avatar={account.aviUrl}
             />
             <View style={[s.ml10]}>
               <Text type="xl-bold" style={pal.text}>
-                {store.me.displayName || store.me.handle}
+                {account.displayName || account.handle}
               </Text>
-              <Text style={pal.textLight}>@{store.me.handle}</Text>
+              <Text style={pal.textLight}>@{account.handle}</Text>
             </View>
+          </TouchableOpacity>
+        ))}
+        <TouchableOpacity
+          testID="switchToNewAccountBtn"
+          style={[
+            pal.view,
+            styles.profile,
+            s.mb2,
+            {alignItems: 'center'},
+            isSwitching && styles.dimmed,
+          ]}
+          onPress={isSwitching ? undefined : onPressAddAccount}>
+          <FontAwesomeIcon icon="plus" />
+          <View style={[s.ml5]}>
+            <Text type="md-medium" style={pal.text}>
+              Add account
+            </Text>
           </View>
-        </Link>
-        <View style={s.flex1} />
+        </TouchableOpacity>
+        <View style={{height: 50}} />
         <Text type="sm-medium" style={[s.mb5]}>
           Developer tools
         </Text>
@@ -80,12 +159,15 @@ export const Settings = observer(function Settings({
           <Text style={pal.link}>Storybook</Text>
         </Link>
         <View style={s.footerSpacer} />
-      </View>
+      </ScrollView>
     </View>
   )
 })
 
 const styles = StyleSheet.create({
+  dimmed: {
+    opacity: 0.5,
+  },
   title: {
     fontSize: 32,
     fontWeight: 'bold',
diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx
index 875bb5a3d..26cb5b9bd 100644
--- a/src/view/shell/mobile/Menu.tsx
+++ b/src/view/shell/mobile/Menu.tsx
@@ -62,7 +62,7 @@ export const Menu = observer(
       onPress?: () => void
     }) => (
       <TouchableOpacity
-        testID="menuItemButton"
+        testID={`menuItemButton-${label}`}
         style={styles.menuItem}
         onPress={onPress ? onPress : () => onNavigate(url || '/')}>
         <View style={[styles.menuItemIconWrapper]}>
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index c4ca7b9f5..b999d05d9 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -5,7 +5,6 @@ import {
   Easing,
   FlatList,
   GestureResponderEvent,
-  SafeAreaView,
   StatusBar,
   StyleSheet,
   TouchableOpacity,
@@ -16,7 +15,6 @@ import {
   ViewStyle,
 } from 'react-native'
 import {ScreenContainer, Screen} from 'react-native-screens'
-import LinearGradient from 'react-native-linear-gradient'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
@@ -34,7 +32,7 @@ import {Text} from '../../com/util/text/Text'
 import {ErrorBoundary} from '../../com/util/ErrorBoundary'
 import {TabsSelector} from './TabsSelector'
 import {Composer} from './Composer'
-import {s, colors} from '../../lib/styles'
+import {colors} from '../../lib/styles'
 import {clamp} from '../../../lib/numbers'
 import {
   GridIcon,
@@ -323,18 +321,10 @@ export const MobileShell: React.FC = observer(() => {
 
   if (!store.session.hasSession) {
     return (
-      <LinearGradient
-        colors={['#007CFF', '#00BCFF']}
-        start={{x: 0, y: 0.8}}
-        end={{x: 0, y: 1}}
-        style={styles.outerContainer}>
-        <SafeAreaView testID="noSessionView" style={styles.innerContainer}>
-          <ErrorBoundary>
-            <Login />
-          </ErrorBoundary>
-        </SafeAreaView>
+      <View style={styles.outerContainer}>
+        <Login />
         <Modal />
-      </LinearGradient>
+      </View>
     )
   }
   if (store.onboard.isOnboarding) {