about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-11-09 20:35:17 -0600
committerEric Bailey <git@esb.lol>2023-11-09 20:35:17 -0600
commitab878ba9a6afaa57805aeab988b01c5b47bc9286 (patch)
tree266f68581a5d237b0d22b4b94fb92e0e45c0993b /src
parent487d871cfd89948f4db9944c4bb414d268a56537 (diff)
downloadvoidsky-ab878ba9a6afaa57805aeab988b01c5b47bc9286.tar.zst
Web login/signup and shell
Diffstat (limited to 'src')
-rw-r--r--src/data/useGetProfile.ts33
-rw-r--r--src/lib/hooks/useAccountSwitcher.ts57
-rw-r--r--src/state/models/ui/create-account.ts11
-rw-r--r--src/state/persisted/schema.ts2
-rw-r--r--src/state/session/index.tsx178
-rw-r--r--src/view/com/auth/LoggedOut.tsx7
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx10
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx113
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx2
-rw-r--r--src/view/com/auth/login/Login.tsx8
-rw-r--r--src/view/com/auth/login/LoginForm.tsx8
-rw-r--r--src/view/com/auth/login/SetNewPasswordForm.tsx2
-rw-r--r--src/view/com/auth/withAuthRequired.tsx8
-rw-r--r--src/view/com/modals/ChangeEmail.tsx25
-rw-r--r--src/view/com/modals/SwitchAccount.tsx147
-rw-r--r--src/view/com/modals/VerifyEmail.tsx20
-rw-r--r--src/view/screens/Settings.tsx236
-rw-r--r--src/view/shell/desktop/LeftNav.tsx63
-rw-r--r--src/view/shell/desktop/RightNav.tsx10
-rw-r--r--src/view/shell/index.tsx7
-rw-r--r--src/view/shell/index.web.tsx6
21 files changed, 580 insertions, 373 deletions
diff --git a/src/data/useGetProfile.ts b/src/data/useGetProfile.ts
new file mode 100644
index 000000000..58f24a4e8
--- /dev/null
+++ b/src/data/useGetProfile.ts
@@ -0,0 +1,33 @@
+import React from 'react'
+import {useQuery} from '@tanstack/react-query'
+import {BskyAgent} from '@atproto/api'
+
+import {useSession} from '#/state/session'
+
+export function useGetProfile({did}: {did: string}) {
+  const {accounts} = useSession()
+  const account = React.useMemo(
+    () => accounts.find(a => a.did === did),
+    [did, accounts],
+  )
+
+  return useQuery({
+    enabled: !!account,
+    queryKey: ['getProfile', account],
+    queryFn: async () => {
+      if (!account) {
+        throw new Error(`useGetProfile: local account not found for ${did}`)
+      }
+
+      const agent = new BskyAgent({
+        // needs to be public data, so remap PDS URLs to App View for now
+        service: account.service.includes('bsky.social')
+          ? 'https://api.bsky.app'
+          : account.service,
+      })
+
+      const res = await agent.getProfile({actor: did})
+      return res.data
+    },
+  })
+}
diff --git a/src/lib/hooks/useAccountSwitcher.ts b/src/lib/hooks/useAccountSwitcher.ts
index b165fddb5..838536735 100644
--- a/src/lib/hooks/useAccountSwitcher.ts
+++ b/src/lib/hooks/useAccountSwitcher.ts
@@ -1,46 +1,43 @@
-import {useCallback, useState} from 'react'
-import {useStores} from 'state/index'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {StackActions, useNavigation} from '@react-navigation/native'
-import {NavigationProp} from 'lib/routes/types'
-import {AccountData} from 'state/models/session'
-import {reset as resetNavigation} from '../../Navigation'
-import * as Toast from 'view/com/util/Toast'
+import {useCallback} from 'react'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {useStores} from '#/state/index'
 import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useModalControls} from '#/state/modals'
+import {useSessionApi, SessionAccount} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
 
-export function useAccountSwitcher(): [
-  boolean,
-  (v: boolean) => void,
-  (acct: AccountData) => Promise<void>,
-] {
+export function useAccountSwitcher() {
   const {track} = useAnalytics()
   const store = useStores()
   const setDrawerOpen = useSetDrawerOpen()
   const {closeModal} = useModalControls()
-  const [isSwitching, setIsSwitching] = useState(false)
-  const navigation = useNavigation<NavigationProp>()
+  const {selectAccount, clearCurrentAccount} = useSessionApi()
 
   const onPressSwitchAccount = useCallback(
-    async (acct: AccountData) => {
+    async (acct: SessionAccount) => {
       track('Settings:SwitchAccountButtonClicked')
-      setIsSwitching(true)
-      const success = await store.session.resumeSession(acct)
-      setDrawerOpen(false)
-      closeModal()
-      store.shell.closeAllActiveElements()
-      if (success) {
-        resetNavigation()
-        Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
-      } else {
+
+      try {
+        await selectAccount(acct)
+        setDrawerOpen(false)
+        closeModal()
+        store.shell.closeAllActiveElements()
+        Toast.show(`Signed in as ${acct.handle}`)
+      } catch (e) {
         Toast.show('Sorry! We need you to enter your password.')
-        navigation.navigate('HomeTab')
-        navigation.dispatch(StackActions.popToTop())
-        store.session.clear()
+        clearCurrentAccount() // back user out to login
       }
     },
-    [track, setIsSwitching, navigation, store, setDrawerOpen, closeModal],
+    [
+      track,
+      store,
+      setDrawerOpen,
+      closeModal,
+      clearCurrentAccount,
+      selectAccount,
+    ],
   )
 
-  return [isSwitching, setIsSwitching, onPressSwitchAccount]
+  return {onPressSwitchAccount}
 }
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index 39c881db6..6d76784c1 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -10,6 +10,7 @@ import {getAge} from 'lib/strings/time'
 import {track} from 'lib/analytics/analytics'
 import {logger} from '#/logger'
 import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
+import {ApiContext as SessionApiContext} from '#/state/session'
 
 const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
 
@@ -91,7 +92,13 @@ export class CreateAccountModel {
     }
   }
 
-  async submit(onboardingDispatch: OnboardingDispatchContext) {
+  async submit({
+    createAccount,
+    onboardingDispatch,
+  }: {
+    createAccount: SessionApiContext['createAccount']
+    onboardingDispatch: OnboardingDispatchContext
+  }) {
     if (!this.email) {
       this.setStep(2)
       return this.setError('Please enter your email.')
@@ -113,7 +120,7 @@ export class CreateAccountModel {
 
     try {
       onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
-      await this.rootStore.session.createAccount({
+      await createAccount({
         service: this.serviceUrl,
         email: this.email,
         handle: createFullHandle(this.handle, this.userDomain),
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index a510262fb..93547aa5b 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -7,6 +7,8 @@ const accountSchema = z.object({
   service: z.string(),
   did: z.string(),
   handle: z.string(),
+  email: z.string(),
+  emailConfirmed: z.boolean(),
   refreshJwt: z.string().optional(), // optional because it can expire
   accessJwt: z.string().optional(), // optional because it can expire
   // displayName: z.string().optional(),
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 8e1f9c1a1..668d9d8ca 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -8,8 +8,9 @@ import * as persisted from '#/state/persisted'
 export type SessionAccount = persisted.PersistedAccount
 
 export type StateContext = {
-  isInitialLoad: boolean
   agent: BskyAgent
+  isInitialLoad: boolean
+  isSwitchingAccounts: boolean
   accounts: persisted.PersistedAccount[]
   currentAccount: persisted.PersistedAccount | undefined
   hasSession: boolean
@@ -33,9 +34,13 @@ export type ApiContext = {
   removeAccount: (
     account: Partial<Pick<persisted.PersistedAccount, 'handle' | 'did'>>,
   ) => void
+  selectAccount: (account: persisted.PersistedAccount) => Promise<void>
   updateCurrentAccount: (
-    account: Pick<persisted.PersistedAccount, 'handle'>,
+    account: Partial<
+      Pick<persisted.PersistedAccount, 'handle' | 'email' | 'emailConfirmed'>
+    >,
   ) => void
+  clearCurrentAccount: () => void
 }
 
 export const PUBLIC_BSKY_AGENT = new BskyAgent({
@@ -43,11 +48,12 @@ export const PUBLIC_BSKY_AGENT = new BskyAgent({
 })
 
 const StateContext = React.createContext<StateContext>({
+  agent: PUBLIC_BSKY_AGENT,
   hasSession: false,
   isInitialLoad: true,
+  isSwitchingAccounts: false,
   accounts: [],
   currentAccount: undefined,
-  agent: PUBLIC_BSKY_AGENT,
 })
 
 const ApiContext = React.createContext<ApiContext>({
@@ -57,7 +63,9 @@ const ApiContext = React.createContext<ApiContext>({
   initSession: async () => {},
   resumeSession: async () => {},
   removeAccount: () => {},
+  selectAccount: async () => {},
   updateCurrentAccount: () => {},
+  clearCurrentAccount: () => {},
 })
 
 function createPersistSessionHandler(
@@ -73,15 +81,21 @@ function createPersistSessionHandler(
       service: account.service,
       did: session?.did || account.did,
       handle: session?.handle || account.handle,
+      email: session?.email || account.email,
+      emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
       refreshJwt: session?.refreshJwt, // undefined when expired or creation fails
       accessJwt: session?.accessJwt, // undefined when expired or creation fails
     }
 
-    logger.debug(`session: BskyAgent.persistSession`, {
-      expired,
-      did: refreshedAccount.did,
-      handle: refreshedAccount.handle,
-    })
+    logger.debug(
+      `session: BskyAgent.persistSession`,
+      {
+        expired,
+        did: refreshedAccount.did,
+        handle: refreshedAccount.handle,
+      },
+      logger.DebugContext.session,
+    )
 
     persistSessionCallback({
       expired,
@@ -92,11 +106,12 @@ function createPersistSessionHandler(
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   const [state, setState] = React.useState<StateContext>({
+    agent: PUBLIC_BSKY_AGENT,
     hasSession: false,
     isInitialLoad: true, // try to resume the session first
+    isSwitchingAccounts: false,
     accounts: persisted.get('session').accounts,
     currentAccount: undefined, // assume logged out to start
-    agent: PUBLIC_BSKY_AGENT,
   })
 
   const upsertAccount = React.useCallback(
@@ -115,10 +130,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   // TODO have not connected this yet
   const createAccount = React.useCallback<ApiContext['createAccount']>(
     async ({service, email, password, handle, inviteCode}: any) => {
-      logger.debug(`session: creating account`, {
-        service,
-        handle,
-      })
+      logger.debug(
+        `session: creating account`,
+        {
+          service,
+          handle,
+        },
+        logger.DebugContext.session,
+      )
 
       const agent = new BskyAgent({service})
 
@@ -136,9 +155,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       const account: persisted.PersistedAccount = {
         service,
         did: agent.session.did,
+        handle: agent.session.handle,
+        email: agent.session.email!, // TODO this is always defined?
+        emailConfirmed: false,
         refreshJwt: agent.session.refreshJwt,
         accessJwt: agent.session.accessJwt,
-        handle: agent.session.handle,
       }
 
       agent.setPersistSessionHandler(
@@ -149,20 +170,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
       upsertAccount(account)
 
-      logger.debug(`session: created account`, {
-        service,
-        handle,
-      })
+      logger.debug(
+        `session: created account`,
+        {
+          service,
+          handle,
+        },
+        logger.DebugContext.session,
+      )
     },
     [upsertAccount],
   )
 
   const login = React.useCallback<ApiContext['login']>(
     async ({service, identifier, password}) => {
-      logger.debug(`session: login`, {
-        service,
-        identifier,
-      })
+      logger.debug(
+        `session: login`,
+        {
+          service,
+          identifier,
+        },
+        logger.DebugContext.session,
+      )
 
       const agent = new BskyAgent({service})
 
@@ -175,9 +204,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       const account: persisted.PersistedAccount = {
         service,
         did: agent.session.did,
+        handle: agent.session.handle,
+        email: agent.session.email!, // TODO this is always defined?
+        emailConfirmed: agent.session.emailConfirmed || false,
         refreshJwt: agent.session.refreshJwt,
         accessJwt: agent.session.accessJwt,
-        handle: agent.session.handle,
       }
 
       agent.setPersistSessionHandler(
@@ -189,16 +220,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       setState(s => ({...s, agent}))
       upsertAccount(account)
 
-      logger.debug(`session: logged in`, {
-        service,
-        identifier,
-      })
+      logger.debug(
+        `session: logged in`,
+        {
+          service,
+          identifier,
+        },
+        logger.DebugContext.session,
+      )
     },
     [upsertAccount],
   )
 
   const logout = React.useCallback<ApiContext['logout']>(async () => {
-    logger.debug(`session: logout`)
+    logger.debug(`session: logout`, {}, logger.DebugContext.session)
     setState(s => {
       return {
         ...s,
@@ -215,10 +250,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
   const initSession = React.useCallback<ApiContext['initSession']>(
     async account => {
-      logger.debug(`session: initSession`, {
-        did: account.did,
-        handle: account.handle,
-      })
+      logger.debug(
+        `session: initSession`,
+        {
+          did: account.did,
+          handle: account.handle,
+        },
+        logger.DebugContext.session,
+      )
 
       const agent = new BskyAgent({
         service: account.service,
@@ -289,19 +328,50 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
         const updatedAccount = {
           ...currentAccount,
-          handle: account.handle, // only update handle rn
+          handle: account.handle || currentAccount.handle,
+          email: account.email || currentAccount.email,
+          emailConfirmed:
+            account.emailConfirmed !== undefined
+              ? account.emailConfirmed
+              : currentAccount.emailConfirmed,
         }
 
         return {
           ...s,
           currentAccount: updatedAccount,
-          accounts: s.accounts.filter(a => a.did !== currentAccount.did),
+          accounts: [
+            updatedAccount,
+            ...s.accounts.filter(a => a.did !== currentAccount.did),
+          ],
         }
       })
     },
     [setState],
   )
 
+  const selectAccount = React.useCallback<ApiContext['selectAccount']>(
+    async account => {
+      setState(s => ({...s, isSwitchingAccounts: true}))
+      try {
+        await initSession(account)
+        setState(s => ({...s, isSwitchingAccounts: false}))
+      } catch (e) {
+        // reset this in case of error
+        setState(s => ({...s, isSwitchingAccounts: false}))
+        // but other listeners need a throw
+        throw e
+      }
+    },
+    [setState, initSession],
+  )
+
+  const clearCurrentAccount = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      currentAccount: undefined,
+    }))
+  }, [setState])
+
   React.useEffect(() => {
     persisted.write('session', {
       accounts: state.accounts,
@@ -313,28 +383,36 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
     return persisted.onUpdate(() => {
       const session = persisted.get('session')
 
-      logger.debug(`session: onUpdate`)
+      logger.debug(`session: onUpdate`, {}, logger.DebugContext.session)
 
       if (session.currentAccount) {
         if (session.currentAccount?.did !== state.currentAccount?.did) {
-          logger.debug(`session: switching account`, {
-            from: {
-              did: state.currentAccount?.did,
-              handle: state.currentAccount?.handle,
+          logger.debug(
+            `session: switching account`,
+            {
+              from: {
+                did: state.currentAccount?.did,
+                handle: state.currentAccount?.handle,
+              },
+              to: {
+                did: session.currentAccount.did,
+                handle: session.currentAccount.handle,
+              },
             },
-            to: {
-              did: session.currentAccount.did,
-              handle: session.currentAccount.handle,
-            },
-          })
+            logger.DebugContext.session,
+          )
 
           initSession(session.currentAccount)
         }
       } else if (!session.currentAccount && state.currentAccount) {
-        logger.debug(`session: logging out`, {
-          did: state.currentAccount?.did,
-          handle: state.currentAccount?.handle,
-        })
+        logger.debug(
+          `session: logging out`,
+          {
+            did: state.currentAccount?.did,
+            handle: state.currentAccount?.handle,
+          },
+          logger.DebugContext.session,
+        )
 
         logout()
       }
@@ -357,7 +435,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       initSession,
       resumeSession,
       removeAccount,
+      selectAccount,
       updateCurrentAccount,
+      clearCurrentAccount,
     }),
     [
       createAccount,
@@ -366,7 +446,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       initSession,
       resumeSession,
       removeAccount,
+      selectAccount,
       updateCurrentAccount,
+      clearCurrentAccount,
     ],
   )
 
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 3e2c9c1bf..0d8172964 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -6,7 +6,6 @@ import {CreateAccount} from 'view/com/auth/create/CreateAccount'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {SplashScreen} from './SplashScreen'
 import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
@@ -19,7 +18,6 @@ enum ScreenState {
 
 export const LoggedOut = observer(function LoggedOutImpl() {
   const pal = usePalette('default')
-  const store = useStores()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {screen} = useAnalytics()
   const [screenState, setScreenState] = React.useState<ScreenState>(
@@ -31,10 +29,7 @@ export const LoggedOut = observer(function LoggedOutImpl() {
     setMinimalShellMode(true)
   }, [screen, setMinimalShellMode])
 
-  if (
-    store.session.isResumingSession ||
-    screenState === ScreenState.S_LoginOrCreateAccount
-  ) {
+  if (screenState === ScreenState.S_LoginOrCreateAccount) {
     return (
       <SplashScreen
         onPressSignin={() => setScreenState(ScreenState.S_Login)}
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 8e2bbed85..0f56755df 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -18,6 +18,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useOnboardingDispatch} from '#/state/shell'
+import {useSessionApi} from '#/state/session'
 
 import {Step1} from './Step1'
 import {Step2} from './Step2'
@@ -34,6 +35,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
   const model = React.useMemo(() => new CreateAccountModel(store), [store])
   const {_} = useLingui()
   const onboardingDispatch = useOnboardingDispatch()
+  const {createAccount} = useSessionApi()
 
   React.useEffect(() => {
     screen('CreateAccount')
@@ -64,14 +66,18 @@ export const CreateAccount = observer(function CreateAccountImpl({
       model.next()
     } else {
       try {
-        await model.submit(onboardingDispatch)
+        console.log('BEFORE')
+        await model.submit({
+          onboardingDispatch,
+          createAccount,
+        })
       } catch {
         // dont need to handle here
       } finally {
         track('Try Create Account')
       }
     }
-  }, [model, track, onboardingDispatch])
+  }, [model, track, onboardingDispatch, createAccount])
 
   return (
     <LoggedOutLayout
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx
index 596a8e411..38c13ba09 100644
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ b/src/view/com/auth/login/ChooseAccountForm.tsx
@@ -1,52 +1,93 @@
 import React from 'react'
-import {
-  ActivityIndicator,
-  ScrollView,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {ScrollView, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {UserAvatar} from '../../util/UserAvatar'
 import {s} from 'lib/styles'
-import {RootStoreModel} from 'state/index'
 import {AccountData} from 'state/models/session'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {styles} from './styles'
+import {useSession, useSessionApi, SessionAccount} from '#/state/session'
+import {useGetProfile} from '#/data/useGetProfile'
 
+function AccountItem({
+  account,
+  onSelect,
+}: {
+  account: SessionAccount
+  onSelect: (account: SessionAccount) => void
+}) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {isError, data} = useGetProfile({did: account.did})
+
+  const onPress = React.useCallback(() => {
+    onSelect(account)
+  }, [account, onSelect])
+
+  if (isError) return null
+
+  return (
+    <TouchableOpacity
+      testID={`chooseAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[pal.view, pal.border, styles.account]}
+      onPress={onPress}
+      accessibilityRole="button"
+      accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
+      accessibilityHint="Double tap to sign in">
+      <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
+        <View style={s.p10}>
+          <UserAvatar avatar={data?.avatar} size={30} />
+        </View>
+        <Text style={styles.accountText}>
+          <Text type="lg-bold" style={pal.text}>
+            {data?.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>
+  )
+}
 export const ChooseAccountForm = ({
-  store,
   onSelectAccount,
   onPressBack,
 }: {
-  store: RootStoreModel
   onSelectAccount: (account?: AccountData) => void
   onPressBack: () => void
 }) => {
   const {track, screen} = useAnalytics()
   const pal = usePalette('default')
-  const [isProcessing, setIsProcessing] = React.useState(false)
   const {_} = useLingui()
+  const {accounts} = useSession()
+  const {initSession} = useSessionApi()
 
   React.useEffect(() => {
     screen('Choose Account')
   }, [screen])
 
-  const onTryAccount = async (account: AccountData) => {
-    if (account.accessJwt && account.refreshJwt) {
-      setIsProcessing(true)
-      if (await store.session.resumeSession(account)) {
+  const onSelect = React.useCallback(
+    async (account: SessionAccount) => {
+      if (account.accessJwt) {
+        await initSession(account)
         track('Sign In', {resumedSession: true})
-        setIsProcessing(false)
-        return
+      } else {
+        onSelectAccount(account)
       }
-      setIsProcessing(false)
-    }
-    onSelectAccount(account)
-  }
+    },
+    [track, initSession, onSelectAccount],
+  )
 
   return (
     <ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
@@ -55,35 +96,8 @@ export const ChooseAccountForm = ({
         style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
         <Trans>Sign in as...</Trans>
       </Text>
-      {store.session.accounts.map(account => (
-        <TouchableOpacity
-          testID={`chooseAccountBtn-${account.handle}`}
-          key={account.did}
-          style={[pal.view, pal.border, styles.account]}
-          onPress={() => onTryAccount(account)}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
-          accessibilityHint="Double tap to sign in">
-          <View
-            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-            <View style={s.p10}>
-              <UserAvatar 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>
+      {accounts.map(account => (
+        <AccountItem key={account.did} account={account} onSelect={onSelect} />
       ))}
       <TouchableOpacity
         testID="chooseNewAccountBtn"
@@ -112,7 +126,6 @@ export const ChooseAccountForm = ({
           </Text>
         </TouchableOpacity>
         <View style={s.flex1} />
-        {isProcessing && <ActivityIndicator />}
       </View>
     </ScrollView>
   )
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
index 9bfab18b5..a794665c9 100644
--- a/src/view/com/auth/login/ForgotPasswordForm.tsx
+++ b/src/view/com/auth/login/ForgotPasswordForm.tsx
@@ -15,7 +15,6 @@ 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 {RootStoreModel} from 'state/index'
 import {ServiceDescription} from 'state/models/session'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -36,7 +35,6 @@ export const ForgotPasswordForm = ({
   onPressBack,
   onEmailSent,
 }: {
-  store: RootStoreModel
   error: string
   serviceUrl: string
   serviceDescription: ServiceDescription | undefined
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
index 401b7d980..de00d6ca2 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -14,6 +14,7 @@ import {SetNewPasswordForm} from './SetNewPasswordForm'
 import {PasswordUpdatedForm} from './PasswordUpdatedForm'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
+import {useSession} from '#/state/session'
 
 enum Forms {
   Login,
@@ -26,6 +27,7 @@ enum Forms {
 export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   const pal = usePalette('default')
   const store = useStores()
+  const {accounts} = useSession()
   const {track} = useAnalytics()
   const {_} = useLingui()
   const [error, setError] = useState<string>('')
@@ -36,7 +38,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   >(undefined)
   const [initialHandle, setInitialHandle] = useState<string>('')
   const [currentForm, setCurrentForm] = useState<Forms>(
-    store.session.hasAccounts ? Forms.ChooseAccount : Forms.Login,
+    accounts.length ? Forms.ChooseAccount : Forms.Login,
   )
 
   const onSelectAccount = (account?: AccountData) => {
@@ -95,7 +97,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Sign in`)}
           description={_(msg`Enter your username and password`)}>
           <LoginForm
-            store={store}
             error={error}
             serviceUrl={serviceUrl}
             serviceDescription={serviceDescription}
@@ -114,7 +115,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Sign in as...`)}
           description={_(msg`Select from an existing account`)}>
           <ChooseAccountForm
-            store={store}
             onSelectAccount={onSelectAccount}
             onPressBack={onPressBack}
           />
@@ -126,7 +126,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Forgot Password`)}
           description={_(msg`Let's get your password reset!`)}>
           <ForgotPasswordForm
-            store={store}
             error={error}
             serviceUrl={serviceUrl}
             serviceDescription={serviceDescription}
@@ -143,7 +142,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Forgot Password`)}
           description={_(msg`Let's get your password reset!`)}>
           <SetNewPasswordForm
-            store={store}
             error={error}
             serviceUrl={serviceUrl}
             setError={setError}
diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx
index 166a7cbd8..be3a95131 100644
--- a/src/view/com/auth/login/LoginForm.tsx
+++ b/src/view/com/auth/login/LoginForm.tsx
@@ -15,7 +15,6 @@ import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
 import {createFullHandle} from 'lib/strings/handles'
 import {toNiceDomain} from 'lib/strings/url-helpers'
-import {RootStoreModel} from 'state/index'
 import {ServiceDescription} from 'state/models/session'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -29,7 +28,6 @@ import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
 
 export const LoginForm = ({
-  store,
   error,
   serviceUrl,
   serviceDescription,
@@ -40,7 +38,6 @@ export const LoginForm = ({
   onPressBack,
   onPressForgotPassword,
 }: {
-  store: RootStoreModel
   error: string
   serviceUrl: string
   serviceDescription: ServiceDescription | undefined
@@ -106,11 +103,6 @@ export const LoginForm = ({
         identifier: fullIdent,
         password,
       })
-      await store.session.login({
-        service: serviceUrl,
-        identifier: fullIdent,
-        password,
-      })
     } catch (e: any) {
       const errMsg = e.toString()
       logger.warn('Failed to login', {error: e})
diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx
index 04eaa2842..2bb614df2 100644
--- a/src/view/com/auth/login/SetNewPasswordForm.tsx
+++ b/src/view/com/auth/login/SetNewPasswordForm.tsx
@@ -10,7 +10,6 @@ import {BskyAgent} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
-import {RootStoreModel} from 'state/index'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
@@ -27,7 +26,6 @@ export const SetNewPasswordForm = ({
   onPressBack,
   onPasswordSet,
 }: {
-  store: RootStoreModel
   error: string
   serviceUrl: string
   setError: (v: string) => void
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
index 898f81051..4b8b31d6c 100644
--- a/src/view/com/auth/withAuthRequired.tsx
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -6,7 +6,6 @@ import {
   TouchableOpacity,
 } from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
 import {CenteredView} from '../util/Views'
 import {LoggedOut} from './LoggedOut'
 import {Onboarding} from './Onboarding'
@@ -14,17 +13,18 @@ import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {STATUS_PAGE_URL} from 'lib/constants'
 import {useOnboardingState} from '#/state/shell'
+import {useSession} from '#/state/session'
 
 export const withAuthRequired = <P extends object>(
   Component: React.ComponentType<P>,
 ): React.FC<P> =>
   observer(function AuthRequired(props: P) {
-    const store = useStores()
+    const {isInitialLoad, hasSession} = useSession()
     const onboardingState = useOnboardingState()
-    if (store.session.isResumingSession) {
+    if (isInitialLoad) {
       return <Loading />
     }
-    if (!store.session.hasSession) {
+    if (!hasSession) {
       return <LoggedOut />
     }
     if (onboardingState.isActive) {
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx
index 710c0588e..6f7a92102 100644
--- a/src/view/com/modals/ChangeEmail.tsx
+++ b/src/view/com/modals/ChangeEmail.tsx
@@ -6,7 +6,6 @@ import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
@@ -15,6 +14,7 @@ import {cleanError} from 'lib/strings/errors'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi} from '#/state/session'
 
 enum Stages {
   InputEmail,
@@ -26,12 +26,11 @@ export const snapPoints = ['90%']
 
 export const Component = observer(function Component({}: {}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {agent, currentAccount} = useSession()
+  const {updateCurrentAccount} = useSessionApi()
   const {_} = useLingui()
   const [stage, setStage] = useState<Stages>(Stages.InputEmail)
-  const [email, setEmail] = useState<string>(
-    store.session.currentSession?.email || '',
-  )
+  const [email, setEmail] = useState<string>(currentAccount?.email || '')
   const [confirmationCode, setConfirmationCode] = useState<string>('')
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
@@ -39,19 +38,19 @@ export const Component = observer(function Component({}: {}) {
   const {openModal, closeModal} = useModalControls()
 
   const onRequestChange = async () => {
-    if (email === store.session.currentSession?.email) {
+    if (email === currentAccount?.email) {
       setError('Enter your new email above')
       return
     }
     setError('')
     setIsProcessing(true)
     try {
-      const res = await store.agent.com.atproto.server.requestEmailUpdate()
+      const res = await agent.com.atproto.server.requestEmailUpdate()
       if (res.data.tokenRequired) {
         setStage(Stages.ConfirmCode)
       } else {
-        await store.agent.com.atproto.server.updateEmail({email: email.trim()})
-        store.session.updateLocalAccountData({
+        await agent.com.atproto.server.updateEmail({email: email.trim()})
+        updateCurrentAccount({
           email: email.trim(),
           emailConfirmed: false,
         })
@@ -79,11 +78,11 @@ export const Component = observer(function Component({}: {}) {
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.updateEmail({
+      await agent.com.atproto.server.updateEmail({
         email: email.trim(),
         token: confirmationCode.trim(),
       })
-      store.session.updateLocalAccountData({
+      updateCurrentAccount({
         email: email.trim(),
         emailConfirmed: false,
       })
@@ -120,8 +119,8 @@ export const Component = observer(function Component({}: {}) {
           ) : stage === Stages.ConfirmCode ? (
             <Trans>
               An email has been sent to your previous address,{' '}
-              {store.session.currentSession?.email || ''}. It includes a
-              confirmation code which you can enter below.
+              {currentAccount?.email || ''}. It includes a confirmation code
+              which you can enter below.
             </Trans>
           ) : (
             <Trans>
diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx
index 1d9457995..2ff70eea4 100644
--- a/src/view/com/modals/SwitchAccount.tsx
+++ b/src/view/com/modals/SwitchAccount.tsx
@@ -6,7 +6,6 @@ import {
   View,
 } from 'react-native'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
@@ -19,26 +18,94 @@ import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
 import {Haptics} from 'lib/haptics'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useSession, useSessionApi, SessionAccount} from '#/state/session'
+import {useGetProfile} from '#/data/useGetProfile'
 
 export const snapPoints = ['40%', '90%']
 
-export function Component({}: {}) {
+function SwitchAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {track} = useAnalytics()
-  const {_: _lingui} = useLingui()
+  const {isSwitchingAccounts, currentAccount} = useSession()
+  const {logout} = useSessionApi()
+  const {isError, data: profile} = useGetProfile({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  const onPressSignout = React.useCallback(() => {
+    track('Settings:SignOutButtonClicked')
+    logout()
+  }, [track, logout])
+
+  // TODO
+  if (isError || !currentAccount) return null
+
+  const contents = (
+    <View style={[pal.view, styles.linkCard]}>
+      <View style={styles.avi}>
+        <UserAvatar size={40} avatar={profile?.avatar} />
+      </View>
+      <View style={[s.flex1]}>
+        <Text type="md-bold" style={pal.text} numberOfLines={1}>
+          {profile?.displayName || currentAccount.handle}
+        </Text>
+        <Text type="sm" style={pal.textLight} numberOfLines={1}>
+          {currentAccount.handle}
+        </Text>
+      </View>
+
+      {isCurrentAccount ? (
+        <TouchableOpacity
+          testID="signOutBtn"
+          onPress={isSwitchingAccounts ? undefined : onPressSignout}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Sign out`)}
+          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
+          <Text type="lg" style={pal.link}>
+            <Trans>Sign out</Trans>
+          </Text>
+        </TouchableOpacity>
+      ) : (
+        <AccountDropdownBtn handle={account.handle} />
+      )}
+    </View>
+  )
 
-  const store = useStores()
-  const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
+  return isCurrentAccount ? (
+    <Link
+      href={makeProfileLink({
+        did: currentAccount.did,
+        handle: currentAccount.handle,
+      })}
+      title="Your profile"
+      noFeedback>
+      {contents}
+    </Link>
+  ) : (
+    <TouchableOpacity
+      testID={`switchToAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[isSwitchingAccounts && styles.dimmed]}
+      onPress={
+        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+      }
+      accessibilityRole="button"
+      accessibilityLabel={`Switch to ${account.handle}`}
+      accessibilityHint="Switches the account you are logged in to">
+      {contents}
+    </TouchableOpacity>
+  )
+}
+
+export function Component({}: {}) {
+  const pal = usePalette('default')
+  const {isSwitchingAccounts, currentAccount, accounts} = useSession()
 
   React.useEffect(() => {
     Haptics.default()
   })
 
-  const onPressSignout = React.useCallback(() => {
-    track('Settings:SignOutButtonClicked')
-    store.session.logout()
-  }, [track, store])
-
   return (
     <BottomSheetScrollView
       style={[styles.container, pal.view]}
@@ -46,62 +113,20 @@ export function Component({}: {}) {
       <Text type="title-xl" style={[styles.title, pal.text]}>
         <Trans>Switch Account</Trans>
       </Text>
-      {isSwitching ? (
+
+      {isSwitchingAccounts || !currentAccount ? (
         <View style={[pal.view, styles.linkCard]}>
           <ActivityIndicator />
         </View>
       ) : (
-        <Link href={makeProfileLink(store.me)} title="Your profile" noFeedback>
-          <View style={[pal.view, styles.linkCard]}>
-            <View style={styles.avi}>
-              <UserAvatar size={40} avatar={store.me.avatar} />
-            </View>
-            <View style={[s.flex1]}>
-              <Text type="md-bold" style={pal.text} numberOfLines={1}>
-                {store.me.displayName || store.me.handle}
-              </Text>
-              <Text type="sm" style={pal.textLight} numberOfLines={1}>
-                {store.me.handle}
-              </Text>
-            </View>
-            <TouchableOpacity
-              testID="signOutBtn"
-              onPress={isSwitching ? undefined : onPressSignout}
-              accessibilityRole="button"
-              accessibilityLabel={_lingui(msg`Sign out`)}
-              accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
-              <Text type="lg" style={pal.link}>
-                <Trans>Sign out</Trans>
-              </Text>
-            </TouchableOpacity>
-          </View>
-        </Link>
+        <SwitchAccountCard account={currentAccount} />
       )}
-      {store.session.switchableAccounts.map(account => (
-        <TouchableOpacity
-          testID={`switchToAccountBtn-${account.handle}`}
-          key={account.did}
-          style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
-          onPress={
-            isSwitching ? undefined : () => onPressSwitchAccount(account)
-          }
-          accessibilityRole="button"
-          accessibilityLabel={`Switch to ${account.handle}`}
-          accessibilityHint="Switches the account you are logged in to">
-          <View style={styles.avi}>
-            <UserAvatar size={40} avatar={account.aviUrl} />
-          </View>
-          <View style={[s.flex1]}>
-            <Text type="md-bold" style={pal.text}>
-              {account.displayName || account.handle}
-            </Text>
-            <Text type="sm" style={pal.textLight}>
-              {account.handle}
-            </Text>
-          </View>
-          <AccountDropdownBtn handle={account.handle} />
-        </TouchableOpacity>
-      ))}
+
+      {accounts
+        .filter(a => a.did !== currentAccount?.did)
+        .map(account => (
+          <SwitchAccountCard key={account.did} account={account} />
+        ))}
     </BottomSheetScrollView>
   )
 }
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index e48e0e4a2..106e05b87 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -14,7 +14,6 @@ import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
@@ -23,6 +22,7 @@ import {cleanError} from 'lib/strings/errors'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi} from '#/state/session'
 
 export const snapPoints = ['90%']
 
@@ -38,7 +38,8 @@ export const Component = observer(function Component({
   showReminder?: boolean
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {agent, currentAccount} = useSession()
+  const {updateCurrentAccount} = useSessionApi()
   const {_} = useLingui()
   const [stage, setStage] = useState<Stages>(
     showReminder ? Stages.Reminder : Stages.Email,
@@ -53,7 +54,7 @@ export const Component = observer(function Component({
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.requestEmailConfirmation()
+      await agent.com.atproto.server.requestEmailConfirmation()
       setStage(Stages.ConfirmCode)
     } catch (e) {
       setError(cleanError(String(e)))
@@ -66,11 +67,11 @@ export const Component = observer(function Component({
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.confirmEmail({
-        email: (store.session.currentSession?.email || '').trim(),
+      await agent.com.atproto.server.confirmEmail({
+        email: (currentAccount?.email || '').trim(),
         token: confirmationCode.trim(),
       })
-      store.session.updateLocalAccountData({emailConfirmed: true})
+      updateCurrentAccount({emailConfirmed: true})
       Toast.show('Email verified')
       closeModal()
     } catch (e) {
@@ -112,9 +113,8 @@ export const Component = observer(function Component({
             </Trans>
           ) : stage === Stages.ConfirmCode ? (
             <Trans>
-              An email has been sent to{' '}
-              {store.session.currentSession?.email || ''}. It includes a
-              confirmation code which you can enter below.
+              An email has been sent to {currentAccount?.email || ''}. It
+              includes a confirmation code which you can enter below.
             </Trans>
           ) : (
             ''
@@ -130,7 +130,7 @@ export const Component = observer(function Component({
                 size={16}
               />
               <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}>
-                {store.session.currentSession?.email || ''}
+                {currentAccount?.email || ''}
               </Text>
             </View>
             <Pressable
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 062533c27..4fd2f2d53 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -57,7 +57,8 @@ import {
   useRequireAltTextEnabled,
   useSetRequireAltTextEnabled,
 } from '#/state/preferences'
-import {useSession, useSessionApi} from '#/state/session'
+import {useSession, useSessionApi, SessionAccount} from '#/state/session'
+import {useGetProfile} from '#/data/useGetProfile'
 
 // TEMPORARY (APP-700)
 // remove after backend testing finishes
@@ -67,6 +68,73 @@ import {STATUS_PAGE_URL} from 'lib/constants'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+function SettingsAccountCard({account}: {account: SessionAccount}) {
+  const pal = usePalette('default')
+  const {isSwitchingAccounts, currentAccount} = useSession()
+  const {logout} = useSessionApi()
+  const {isError, data} = useGetProfile({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  // TODO
+  if (isError || !currentAccount) return null
+
+  const contents = (
+    <View style={[pal.view, styles.linkCard]}>
+      <View style={styles.avi}>
+        <UserAvatar size={40} avatar={data?.avatar} />
+      </View>
+      <View style={[s.flex1]}>
+        <Text type="md-bold" style={pal.text}>
+          {data?.displayName || account.handle}
+        </Text>
+        <Text type="sm" style={pal.textLight}>
+          {account.handle}
+        </Text>
+      </View>
+
+      {isCurrentAccount ? (
+        <TouchableOpacity
+          testID="signOutBtn"
+          onPress={logout}
+          accessibilityRole="button"
+          accessibilityLabel="Sign out"
+          accessibilityHint={`Signs ${data?.displayName} out of Bluesky`}>
+          <Text type="lg" style={pal.link}>
+            Sign out
+          </Text>
+        </TouchableOpacity>
+      ) : (
+        <AccountDropdownBtn handle={account.handle} />
+      )}
+    </View>
+  )
+
+  return isCurrentAccount ? (
+    <Link
+      href={makeProfileLink({
+        did: currentAccount?.did,
+        handle: currentAccount?.handle,
+      })}
+      title="Your profile"
+      noFeedback>
+      {contents}
+    </Link>
+  ) : (
+    <TouchableOpacity
+      testID={`switchToAccountBtn-${account.handle}`}
+      key={account.did}
+      onPress={
+        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+      }
+      accessibilityRole="button"
+      accessibilityLabel={`Switch to ${account.handle}`}
+      accessibilityHint="Switches the account you are logged in to">
+      {contents}
+    </TouchableOpacity>
+  )
+}
+
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
 export const SettingsScreen = withAuthRequired(
   observer(function Settings({}: Props) {
@@ -82,14 +150,12 @@ export const SettingsScreen = withAuthRequired(
     const navigation = useNavigation<NavigationProp>()
     const {isMobile} = useWebMediaQueries()
     const {screen, track} = useAnalytics()
-    const [isSwitching, setIsSwitching, onPressSwitchAccount] =
-      useAccountSwitcher()
     const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
       store.agent,
     )
     const {openModal} = useModalControls()
-    const {logout} = useSessionApi()
-    const {accounts} = useSession()
+    const {isSwitchingAccounts, accounts, currentAccount} = useSession()
+    const {clearCurrentAccount} = useSessionApi()
 
     const primaryBg = useCustomPalette<ViewStyle>({
       light: {backgroundColor: colors.blue0},
@@ -120,30 +186,27 @@ export const SettingsScreen = withAuthRequired(
       track('Settings:AddAccountButtonClicked')
       navigation.navigate('HomeTab')
       navigation.dispatch(StackActions.popToTop())
-      store.session.clear()
-    }, [track, navigation, store])
+      clearCurrentAccount()
+    }, [track, navigation, clearCurrentAccount])
 
     const onPressChangeHandle = React.useCallback(() => {
       track('Settings:ChangeHandleButtonClicked')
       openModal({
         name: 'change-handle',
         onChanged() {
-          setIsSwitching(true)
           store.session.reloadFromServer().then(
             () => {
-              setIsSwitching(false)
               Toast.show('Your handle has been updated')
             },
             err => {
               logger.error('Failed to reload from server after handle update', {
                 error: err,
               })
-              setIsSwitching(false)
             },
           )
         },
       })
-    }, [track, store, openModal, setIsSwitching])
+    }, [track, store, openModal])
 
     const onPressInviteCodes = React.useCallback(() => {
       track('Settings:InvitecodesButtonClicked')
@@ -154,12 +217,6 @@ export const SettingsScreen = withAuthRequired(
       navigation.navigate('LanguageSettings')
     }, [navigation])
 
-    const onPressSignout = React.useCallback(() => {
-      track('Settings:SignOutButtonClicked')
-      logout()
-      store.session.logout()
-    }, [track, store, logout])
-
     const onPressDeleteAccount = React.useCallback(() => {
       openModal({name: 'delete-account'})
     }, [openModal])
@@ -217,7 +274,7 @@ export const SettingsScreen = withAuthRequired(
           contentContainerStyle={isMobile && pal.viewLight}
           scrollIndicatorInsets={{right: 1}}>
           <View style={styles.spacer20} />
-          {store.session.currentSession !== undefined ? (
+          {currentAccount ? (
             <>
               <Text type="xl-bold" style={[pal.text, styles.heading]}>
                 <Trans>Account</Trans>
@@ -226,7 +283,7 @@ export const SettingsScreen = withAuthRequired(
                 <Text type="lg-medium" style={pal.text}>
                   Email:{' '}
                 </Text>
-                {!store.session.emailNeedsConfirmation && (
+                {currentAccount.emailConfirmed && (
                   <>
                     <FontAwesomeIcon
                       icon="check"
@@ -236,7 +293,7 @@ export const SettingsScreen = withAuthRequired(
                   </>
                 )}
                 <Text type="lg" style={pal.text}>
-                  {store.session.currentSession?.email}{' '}
+                  {currentAccount.email}{' '}
                 </Text>
                 <Link onPress={() => openModal({name: 'change-email'})}>
                   <Text type="lg" style={pal.link}>
@@ -255,7 +312,8 @@ export const SettingsScreen = withAuthRequired(
                 </Link>
               </View>
               <View style={styles.spacer20} />
-              <EmailConfirmationNotice />
+
+              {!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
             </>
           ) : null}
           <View style={[s.flexRow, styles.heading]}>
@@ -264,70 +322,29 @@ export const SettingsScreen = withAuthRequired(
             </Text>
             <View style={s.flex1} />
           </View>
-          {isSwitching ? (
+
+          {isSwitchingAccounts ? (
             <View style={[pal.view, styles.linkCard]}>
               <ActivityIndicator />
             </View>
           ) : (
-            <Link
-              href={makeProfileLink(store.me)}
-              title="Your profile"
-              noFeedback>
-              <View style={[pal.view, styles.linkCard]}>
-                <View style={styles.avi}>
-                  <UserAvatar size={40} avatar={store.me.avatar} />
-                </View>
-                <View style={[s.flex1]}>
-                  <Text type="md-bold" style={pal.text} numberOfLines={1}>
-                    {store.me.displayName || store.me.handle}
-                  </Text>
-                  <Text type="sm" style={pal.textLight} numberOfLines={1}>
-                    {store.me.handle}
-                  </Text>
-                </View>
-                <TouchableOpacity
-                  testID="signOutBtn"
-                  onPress={isSwitching ? undefined : onPressSignout}
-                  accessibilityRole="button"
-                  accessibilityLabel={_(msg`Sign out`)}
-                  accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
-                  <Text type="lg" style={pal.link}>
-                    <Trans>Sign out</Trans>
-                  </Text>
-                </TouchableOpacity>
-              </View>
-            </Link>
+            <SettingsAccountCard account={currentAccount!} />
           )}
-          {accounts.map(account => (
-            <TouchableOpacity
-              testID={`switchToAccountBtn-${account.handle}`}
-              key={account.did}
-              style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
-              onPress={
-                isSwitching ? undefined : () => onPressSwitchAccount(account)
-              }
-              accessibilityRole="button"
-              accessibilityLabel={`Switch to ${account.handle}`}
-              accessibilityHint="Switches the account you are logged in to">
-              <View style={styles.avi}>
-                {/*<UserAvatar size={40} avatar={account.aviUrl} />*/}
-              </View>
-              <View style={[s.flex1]}>
-                <Text type="md-bold" style={pal.text}>
-                  {/* @ts-ignore */}
-                  {account.displayName || account.handle}
-                </Text>
-                <Text type="sm" style={pal.textLight}>
-                  {account.handle}
-                </Text>
-              </View>
-              <AccountDropdownBtn handle={account.handle} />
-            </TouchableOpacity>
-          ))}
+
+          {accounts
+            .filter(a => a.did !== currentAccount?.did)
+            .map(account => (
+              <SettingsAccountCard key={account.did} account={account} />
+            ))}
+
           <TouchableOpacity
             testID="switchToNewAccountBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressAddAccount}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
+            onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
             accessibilityRole="button"
             accessibilityLabel={_(msg`Add account`)}
             accessibilityHint="Create a new Bluesky account">
@@ -349,8 +366,12 @@ export const SettingsScreen = withAuthRequired(
           </Text>
           <TouchableOpacity
             testID="inviteFriendBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressInviteCodes}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
+            onPress={isSwitchingAccounts ? undefined : onPressInviteCodes}
             accessibilityRole="button"
             accessibilityLabel={_(msg`Invite`)}
             accessibilityHint="Opens invite code list">
@@ -427,7 +448,11 @@ export const SettingsScreen = withAuthRequired(
           </Text>
           <TouchableOpacity
             testID="preferencesHomeFeedButton"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
             onPress={openHomeFeedPreferences}
             accessibilityRole="button"
             accessibilityHint=""
@@ -444,7 +469,11 @@ export const SettingsScreen = withAuthRequired(
           </TouchableOpacity>
           <TouchableOpacity
             testID="preferencesThreadsButton"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
             onPress={openThreadsPreferences}
             accessibilityRole="button"
             accessibilityHint=""
@@ -462,7 +491,11 @@ export const SettingsScreen = withAuthRequired(
           </TouchableOpacity>
           <TouchableOpacity
             testID="savedFeedsBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
             accessibilityHint="My Saved Feeds"
             accessibilityLabel={_(msg`Opens screen with all saved feeds`)}
             onPress={onPressSavedFeeds}>
@@ -475,8 +508,12 @@ export const SettingsScreen = withAuthRequired(
           </TouchableOpacity>
           <TouchableOpacity
             testID="languageSettingsBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressLanguageSettings}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
+            onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings}
             accessibilityRole="button"
             accessibilityHint="Language settings"
             accessibilityLabel={_(msg`Opens configurable language settings`)}>
@@ -492,9 +529,15 @@ export const SettingsScreen = withAuthRequired(
           </TouchableOpacity>
           <TouchableOpacity
             testID="moderationBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
             onPress={
-              isSwitching ? undefined : () => navigation.navigate('Moderation')
+              isSwitchingAccounts
+                ? undefined
+                : () => navigation.navigate('Moderation')
             }
             accessibilityRole="button"
             accessibilityHint=""
@@ -513,7 +556,11 @@ export const SettingsScreen = withAuthRequired(
           </Text>
           <TouchableOpacity
             testID="appPasswordBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
             onPress={onPressAppPasswords}
             accessibilityRole="button"
             accessibilityHint="Open app password settings"
@@ -530,8 +577,12 @@ export const SettingsScreen = withAuthRequired(
           </TouchableOpacity>
           <TouchableOpacity
             testID="changeHandleBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressChangeHandle}
+            style={[
+              styles.linkCard,
+              pal.view,
+              isSwitchingAccounts && styles.dimmed,
+            ]}
+            onPress={isSwitchingAccounts ? undefined : onPressChangeHandle}
             accessibilityRole="button"
             accessibilityLabel={_(msg`Change handle`)}
             accessibilityHint="Choose a new Bluesky username or create">
@@ -655,15 +706,10 @@ const EmailConfirmationNotice = observer(
   function EmailConfirmationNoticeImpl() {
     const pal = usePalette('default')
     const palInverted = usePalette('inverted')
-    const store = useStores()
     const {_} = useLingui()
     const {isMobile} = useWebMediaQueries()
     const {openModal} = useModalControls()
 
-    if (!store.session.emailNeedsConfirmation) {
-      return null
-    }
-
     return (
       <View style={{marginBottom: 20}}>
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index b85823b6f..3a0c0c95d 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -41,18 +41,31 @@ import {router} from '../../../routes'
 import {makeProfileLink} from 'lib/routes/links'
 import {useLingui} from '@lingui/react'
 import {Trans, msg} from '@lingui/macro'
+import {useGetProfile} from '#/data/useGetProfile'
+import {useSession} from '#/state/session'
 
 const ProfileCard = observer(function ProfileCardImpl() {
-  const store = useStores()
+  const {currentAccount} = useSession()
+  const {
+    isLoading,
+    isError,
+    data: profile,
+  } = useGetProfile({did: currentAccount!.did})
   const {isDesktop} = useWebMediaQueries()
   const size = 48
-  return store.me.handle ? (
+
+  if (isError || !profile || !currentAccount) return null
+
+  return !isLoading ? (
     <Link
-      href={makeProfileLink(store.me)}
+      href={makeProfileLink({
+        did: currentAccount.did,
+        handle: currentAccount.handle,
+      })}
       style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}
       title="My Profile"
       asAnchor>
-      <UserAvatar avatar={store.me.avatar} size={size} />
+      <UserAvatar avatar={profile.avatar} size={size} />
     </Link>
   ) : (
     <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}>
@@ -255,7 +268,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         pal.view,
         pal.border,
       ]}>
-      {store.session.hasSession && <ProfileCard />}
+      <ProfileCard />
       <BackBtn />
       <NavItem
         href="/"
@@ -360,26 +373,24 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         }
         label="Moderation"
       />
-      {store.session.hasSession && (
-        <NavItem
-          href={makeProfileLink(store.me)}
-          icon={
-            <UserIcon
-              strokeWidth={1.75}
-              size={isDesktop ? 28 : 30}
-              style={pal.text}
-            />
-          }
-          iconFilled={
-            <UserIconSolid
-              strokeWidth={1.75}
-              size={isDesktop ? 28 : 30}
-              style={pal.text}
-            />
-          }
-          label="Profile"
-        />
-      )}
+      <NavItem
+        href={makeProfileLink(store.me)}
+        icon={
+          <UserIcon
+            strokeWidth={1.75}
+            size={isDesktop ? 28 : 30}
+            style={pal.text}
+          />
+        }
+        iconFilled={
+          <UserIconSolid
+            strokeWidth={1.75}
+            size={isDesktop ? 28 : 30}
+            style={pal.text}
+          />
+        }
+        label="Profile"
+      />
       <NavItem
         href="/settings"
         icon={
@@ -398,7 +409,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         }
         label="Settings"
       />
-      {store.session.hasSession && <ComposeBtn />}
+      <ComposeBtn />
     </View>
   )
 })
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index a4b3e5746..cb62b4be2 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -14,11 +14,13 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {pluralize} from 'lib/strings/helpers'
 import {formatCount} from 'view/com/util/numeric/format'
 import {useModalControls} from '#/state/modals'
+import {useSession} from '#/state/session'
 
 export const DesktopRightNav = observer(function DesktopRightNavImpl() {
   const store = useStores()
   const pal = usePalette('default')
   const palError = usePalette('error')
+  const {hasSession, currentAccount} = useSession()
 
   const {isTablet} = useWebMediaQueries()
   if (isTablet) {
@@ -27,8 +29,8 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
 
   return (
     <View style={[styles.rightNav, pal.view]}>
-      {store.session.hasSession && <DesktopSearch />}
-      {store.session.hasSession && <DesktopFeeds />}
+      {hasSession && <DesktopSearch />}
+      {hasSession && <DesktopFeeds />}
       <View style={styles.message}>
         {store.session.isSandbox ? (
           <View style={[palError.view, styles.messageLine, s.p10]}>
@@ -42,8 +44,8 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
             type="md"
             style={pal.link}
             href={FEEDBACK_FORM_URL({
-              email: store.session.currentSession?.email,
-              handle: store.session.currentSession?.handle,
+              email: currentAccount!.email,
+              handle: currentAccount!.handle,
             })}
             text="Send feedback"
           />
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 498bc11bd..75ed07475 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -33,6 +33,7 @@ import {
 } from '#/state/shell'
 import {isAndroid} from 'platform/detection'
 import {useModalControls} from '#/state/modals'
+import {useSession} from '#/state/session'
 
 const ShellInner = observer(function ShellInnerImpl() {
   const store = useStores()
@@ -57,6 +58,8 @@ const ShellInner = observer(function ShellInnerImpl() {
     [setIsDrawerOpen],
   )
   const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
+  const {hasSession} = useSession()
+
   React.useEffect(() => {
     let listener = {remove() {}}
     if (isAndroid) {
@@ -81,9 +84,7 @@ const ShellInner = observer(function ShellInnerImpl() {
             onOpen={onOpenDrawer}
             onClose={onCloseDrawer}
             swipeEdgeWidth={winDim.width / 2}
-            swipeEnabled={
-              !canGoBack && store.session.hasSession && !isDrawerSwipeDisabled
-            }>
+            swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}>
             <TabsNavigator />
           </Drawer>
         </ErrorBoundary>
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 792499521..a74cd126f 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -24,6 +24,7 @@ import {
   useOnboardingState,
 } from '#/state/shell'
 import {useModalControls} from '#/state/modals'
+import {useSession} from '#/state/session'
 
 const ShellInner = observer(function ShellInnerImpl() {
   const store = useStores()
@@ -33,6 +34,8 @@ const ShellInner = observer(function ShellInnerImpl() {
   const onboardingState = useOnboardingState()
   const {isDesktop, isMobile} = useWebMediaQueries()
   const navigator = useNavigation<NavigationProp>()
+  const {hasSession} = useSession()
+
   useAuxClick()
 
   useEffect(() => {
@@ -44,8 +47,7 @@ const ShellInner = observer(function ShellInnerImpl() {
   }, [navigator, store.shell, setDrawerOpen, closeModal])
 
   const showBottomBar = isMobile && !onboardingState.isActive
-  const showSideNavs =
-    !isMobile && store.session.hasSession && !onboardingState.isActive
+  const showSideNavs = !isMobile && hasSession && !onboardingState.isActive
   return (
     <View style={[s.hContentRegion, {overflow: 'hidden'}]}>
       <View style={s.hContentRegion}>