about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/constants.ts5
-rw-r--r--src/lib/hooks/useAccountSwitcher.ts57
-rw-r--r--src/state/models/root-store.ts20
-rw-r--r--src/state/models/session.ts443
-rw-r--r--src/state/models/ui/create-account.ts11
-rw-r--r--src/state/persisted/schema.ts2
-rw-r--r--src/state/queries/index.ts5
-rw-r--r--src/state/queries/profile.ts13
-rw-r--r--src/state/session/index.tsx250
-rw-r--r--src/view/com/auth/LoggedOut.tsx7
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx9
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx114
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx2
-rw-r--r--src/view/com/auth/login/Login.tsx11
-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/feeds/FeedPage.tsx15
-rw-r--r--src/view/com/modals/ChangeEmail.tsx25
-rw-r--r--src/view/com/modals/SwitchAccount.tsx144
-rw-r--r--src/view/com/modals/VerifyEmail.tsx20
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx4
-rw-r--r--src/view/com/testing/TestCtrls.e2e.tsx8
-rw-r--r--src/view/com/util/AccountDropdownBtn.tsx6
-rw-r--r--src/view/com/util/PostSandboxWarning.tsx6
-rw-r--r--src/view/screens/Settings.tsx233
-rw-r--r--src/view/shell/Drawer.tsx98
-rw-r--r--src/view/shell/desktop/LeftNav.tsx57
-rw-r--r--src/view/shell/desktop/RightNav.tsx13
-rw-r--r--src/view/shell/index.tsx7
-rw-r--r--src/view/shell/index.web.tsx6
31 files changed, 694 insertions, 915 deletions
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 472b59d76..89c441e98 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -43,7 +43,10 @@ export function IS_PROD(url: string) {
   // until open federation, "production" is defined as the main server
   // this definition will not work once federation is enabled!
   // -prf
-  return url.startsWith('https://bsky.social')
+  return (
+    url.startsWith('https://bsky.social') ||
+    url.startsWith('https://api.bsky.app')
+  )
 }
 
 export const PROD_TEAM_HANDLES = [
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/root-store.ts b/src/state/models/root-store.ts
index d11e9a148..4085a52c3 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -63,7 +63,6 @@ export class RootStoreModel {
   serialize(): unknown {
     return {
       appInfo: this.appInfo,
-      session: this.session.serialize(),
       me: this.me.serialize(),
       preferences: this.preferences.serialize(),
     }
@@ -80,9 +79,6 @@ export class RootStoreModel {
       if (hasProp(v, 'me')) {
         this.me.hydrate(v.me)
       }
-      if (hasProp(v, 'session')) {
-        this.session.hydrate(v.session)
-      }
       if (hasProp(v, 'preferences')) {
         this.preferences.hydrate(v.preferences)
       }
@@ -92,18 +88,7 @@ export class RootStoreModel {
   /**
    * Called during init to resume any stored session.
    */
-  async attemptSessionResumption() {
-    logger.debug('RootStoreModel:attemptSessionResumption')
-    try {
-      await this.session.attemptSessionResumption()
-      logger.debug('Session initialized', {
-        hasSession: this.session.hasSession,
-      })
-      this.updateSessionState()
-    } catch (e: any) {
-      logger.warn('Failed to initialize session', {error: e})
-    }
-  }
+  async attemptSessionResumption() {}
 
   /**
    * Called by the session model. Refreshes session-oriented state.
@@ -135,11 +120,10 @@ export class RootStoreModel {
   }
 
   /**
-   * Clears all session-oriented state.
+   * Clears all session-oriented state, previously called on LOGOUT
    */
   clearAllSessionState() {
     logger.debug('RootStoreModel:clearAllSessionState')
-    this.session.clear()
     resetToTab('HomeTab')
     this.me.clear()
   }
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 5b95c7d32..2c66cfdf6 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -1,274 +1,26 @@
-import {makeAutoObservable, runInAction} from 'mobx'
+import {makeAutoObservable} from 'mobx'
 import {
   BskyAgent,
-  AtpSessionEvent,
-  AtpSessionData,
   ComAtprotoServerDescribeServer as DescribeServer,
 } from '@atproto/api'
-import normalizeUrl from 'normalize-url'
-import {isObj, hasProp} from 'lib/type-guards'
-import {networkRetry} from 'lib/async/retry'
-import {z} from 'zod'
 import {RootStoreModel} from './root-store'
-import {IS_PROD} from 'lib/constants'
-import {track} from 'lib/analytics/analytics'
-import {logger} from '#/logger'
 
 export type ServiceDescription = DescribeServer.OutputSchema
 
-export const activeSession = z.object({
-  service: z.string(),
-  did: z.string(),
-})
-export type ActiveSession = z.infer<typeof activeSession>
-
-export const accountData = z.object({
-  service: z.string(),
-  refreshJwt: z.string().optional(),
-  accessJwt: z.string().optional(),
-  handle: z.string(),
-  did: z.string(),
-  email: z.string().optional(),
-  displayName: z.string().optional(),
-  aviUrl: z.string().optional(),
-  emailConfirmed: z.boolean().optional(),
-})
-export type AccountData = z.infer<typeof accountData>
-
-interface AdditionalAccountData {
-  displayName?: string
-  aviUrl?: string
-}
-
 export class SessionModel {
-  // DEBUG
-  // emergency log facility to help us track down this logout issue
-  // remove when resolved
-  // -prf
-  _log(message: string, details?: Record<string, any>) {
-    details = details || {}
-    details.state = {
-      data: this.data,
-      accounts: this.accounts.map(
-        a =>
-          `${!!a.accessJwt && !!a.refreshJwt ? '✅' : '❌'} ${a.handle} (${
-            a.service
-          })`,
-      ),
-      isResumingSession: this.isResumingSession,
-    }
-    logger.debug(message, details, logger.DebugContext.session)
-  }
-
-  /**
-   * Currently-active session
-   */
-  data: ActiveSession | null = null
-  /**
-   * A listing of the currently & previous sessions
-   */
-  accounts: AccountData[] = []
-  /**
-   * Flag to indicate if we're doing our initial-load session resumption
-   */
-  isResumingSession = false
-
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {
       rootStore: false,
-      serialize: false,
-      hydrate: false,
       hasSession: false,
     })
   }
 
-  get currentSession() {
-    if (!this.data) {
-      return undefined
-    }
-    const {did, service} = this.data
-    return this.accounts.find(
-      account =>
-        normalizeUrl(account.service) === normalizeUrl(service) &&
-        account.did === did &&
-        !!account.accessJwt &&
-        !!account.refreshJwt,
-    )
+  get currentSession(): any {
+    return undefined
   }
 
   get hasSession() {
-    return !!this.currentSession && !!this.rootStore.agent.session
-  }
-
-  get hasAccounts() {
-    return this.accounts.length >= 1
-  }
-
-  get switchableAccounts() {
-    return this.accounts.filter(acct => acct.did !== this.data?.did)
-  }
-
-  get emailNeedsConfirmation() {
-    return !this.currentSession?.emailConfirmed
-  }
-
-  get isSandbox() {
-    if (!this.data) {
-      return false
-    }
-    return !IS_PROD(this.data.service)
-  }
-
-  serialize(): unknown {
-    return {
-      data: this.data,
-      accounts: this.accounts,
-    }
-  }
-
-  hydrate(v: unknown) {
-    this.accounts = []
-    if (isObj(v)) {
-      if (hasProp(v, 'data') && activeSession.safeParse(v.data)) {
-        this.data = v.data as ActiveSession
-      }
-      if (hasProp(v, 'accounts') && Array.isArray(v.accounts)) {
-        for (const account of v.accounts) {
-          if (accountData.safeParse(account)) {
-            this.accounts.push(account as AccountData)
-          }
-        }
-      }
-    }
-  }
-
-  clear() {
-    this.data = null
-  }
-
-  /**
-   * Attempts to resume the previous session loaded from storage
-   */
-  async attemptSessionResumption() {
-    const sess = this.currentSession
-    if (sess) {
-      this._log('SessionModel:attemptSessionResumption found stored session')
-      this.isResumingSession = true
-      try {
-        return await this.resumeSession(sess)
-      } finally {
-        runInAction(() => {
-          this.isResumingSession = false
-        })
-      }
-    } else {
-      this._log(
-        'SessionModel:attemptSessionResumption has no session to resume',
-      )
-    }
-  }
-
-  /**
-   * Sets the active session
-   */
-  async setActiveSession(agent: BskyAgent, did: string) {
-    this._log('SessionModel:setActiveSession')
-    const hadSession = !!this.data
-    this.data = {
-      service: agent.service.toString(),
-      did,
-    }
-    await this.rootStore.handleSessionChange(agent, {hadSession})
-  }
-
-  /**
-   * Upserts a session into the accounts
-   */
-  persistSession(
-    service: string,
-    did: string,
-    event: AtpSessionEvent,
-    session?: AtpSessionData,
-    addedInfo?: AdditionalAccountData,
-  ) {
-    this._log('SessionModel:persistSession', {
-      service,
-      did,
-      event,
-      hasSession: !!session,
-    })
-
-    const existingAccount = this.accounts.find(
-      account => account.service === service && account.did === did,
-    )
-
-    // fall back to any preexisting access tokens
-    let refreshJwt = session?.refreshJwt || existingAccount?.refreshJwt
-    let accessJwt = session?.accessJwt || existingAccount?.accessJwt
-    if (event === 'expired') {
-      // only clear the tokens when they're known to have expired
-      refreshJwt = undefined
-      accessJwt = undefined
-    }
-
-    const newAccount = {
-      service,
-      did,
-      refreshJwt,
-      accessJwt,
-
-      handle: session?.handle || existingAccount?.handle || '',
-      email: session?.email || existingAccount?.email || '',
-      displayName: addedInfo
-        ? addedInfo.displayName
-        : existingAccount?.displayName || '',
-      aviUrl: addedInfo ? addedInfo.aviUrl : existingAccount?.aviUrl || '',
-      emailConfirmed: session?.emailConfirmed,
-    }
-    if (!existingAccount) {
-      this.accounts.push(newAccount)
-    } else {
-      this.accounts = [
-        newAccount,
-        ...this.accounts.filter(
-          account => !(account.service === service && account.did === did),
-        ),
-      ]
-    }
-
-    // if the session expired, fire an event to let the user know
-    if (event === 'expired') {
-      this.rootStore.handleSessionDrop()
-    }
-  }
-
-  /**
-   * Clears any session tokens from the accounts; used on logout.
-   */
-  clearSessionTokens() {
-    this._log('SessionModel:clearSessionTokens')
-    this.accounts = this.accounts.map(acct => ({
-      service: acct.service,
-      handle: acct.handle,
-      did: acct.did,
-      displayName: acct.displayName,
-      aviUrl: acct.aviUrl,
-      email: acct.email,
-      emailConfirmed: acct.emailConfirmed,
-    }))
-  }
-
-  /**
-   * Fetches additional information about an account on load.
-   */
-  async loadAccountInfo(agent: BskyAgent, did: string) {
-    const res = await agent.getProfile({actor: did}).catch(_e => undefined)
-    if (res) {
-      return {
-        displayName: res.data.displayName,
-        aviUrl: res.data.avatar,
-      }
-    }
+    return false
   }
 
   /**
@@ -281,192 +33,7 @@ export class SessionModel {
   }
 
   /**
-   * Attempt to resume a session that we still have access tokens for.
-   */
-  async resumeSession(account: AccountData): Promise<boolean> {
-    this._log('SessionModel:resumeSession')
-    if (!(account.accessJwt && account.refreshJwt && account.service)) {
-      this._log(
-        'SessionModel:resumeSession aborted due to lack of access tokens',
-      )
-      return false
-    }
-
-    const agent = new BskyAgent({
-      service: account.service,
-      persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
-        this.persistSession(account.service, account.did, evt, sess)
-      },
-    })
-
-    try {
-      await networkRetry(3, () =>
-        agent.resumeSession({
-          accessJwt: account.accessJwt || '',
-          refreshJwt: account.refreshJwt || '',
-          did: account.did,
-          handle: account.handle,
-          email: account.email,
-          emailConfirmed: account.emailConfirmed,
-        }),
-      )
-      const addedInfo = await this.loadAccountInfo(agent, account.did)
-      this.persistSession(
-        account.service,
-        account.did,
-        'create',
-        agent.session,
-        addedInfo,
-      )
-      this._log('SessionModel:resumeSession succeeded')
-    } catch (e: any) {
-      this._log('SessionModel:resumeSession failed', {
-        error: e.toString(),
-      })
-      return false
-    }
-
-    await this.setActiveSession(agent, account.did)
-    return true
-  }
-
-  /**
-   * Create a new session.
-   */
-  async login({
-    service,
-    identifier,
-    password,
-  }: {
-    service: string
-    identifier: string
-    password: string
-  }) {
-    this._log('SessionModel:login')
-    const agent = new BskyAgent({service})
-    await agent.login({identifier, password})
-    if (!agent.session) {
-      throw new Error('Failed to establish session')
-    }
-
-    const did = agent.session.did
-    const addedInfo = await this.loadAccountInfo(agent, did)
-
-    this.persistSession(service, did, 'create', agent.session, addedInfo)
-    agent.setPersistSessionHandler(
-      (evt: AtpSessionEvent, sess?: AtpSessionData) => {
-        this.persistSession(service, did, evt, sess)
-      },
-    )
-
-    await this.setActiveSession(agent, did)
-    this._log('SessionModel:login succeeded')
-  }
-
-  async createAccount({
-    service,
-    email,
-    password,
-    handle,
-    inviteCode,
-  }: {
-    service: string
-    email: string
-    password: string
-    handle: string
-    inviteCode?: string
-  }) {
-    this._log('SessionModel:createAccount')
-    const agent = new BskyAgent({service})
-    await agent.createAccount({
-      handle,
-      password,
-      email,
-      inviteCode,
-    })
-    if (!agent.session) {
-      throw new Error('Failed to establish session')
-    }
-
-    const did = agent.session.did
-    const addedInfo = await this.loadAccountInfo(agent, did)
-
-    this.persistSession(service, did, 'create', agent.session, addedInfo)
-    agent.setPersistSessionHandler(
-      (evt: AtpSessionEvent, sess?: AtpSessionData) => {
-        this.persistSession(service, did, evt, sess)
-      },
-    )
-
-    await this.setActiveSession(agent, did)
-    this._log('SessionModel:createAccount succeeded')
-    track('Create Account Successfully')
-  }
-
-  /**
-   * Close all sessions across all accounts.
-   */
-  async logout() {
-    this._log('SessionModel:logout')
-    // TODO
-    // need to evaluate why deleting the session has caused errors at times
-    // -prf
-    /*if (this.hasSession) {
-      this.rootStore.agent.com.atproto.session.delete().catch((e: any) => {
-        this.rootStore.log.warn(
-          '(Minor issue) Failed to delete session on the server',
-          e,
-        )
-      })
-    }*/
-    this.clearSessionTokens()
-    this.rootStore.clearAllSessionState()
-  }
-
-  /**
-   * Removes an account from the list of stored accounts.
-   */
-  removeAccount(handle: string) {
-    this.accounts = this.accounts.filter(acc => acc.handle !== handle)
-  }
-
-  /**
    * Reloads the session from the server. Useful when account details change, like the handle.
    */
-  async reloadFromServer() {
-    const sess = this.currentSession
-    if (!sess) {
-      return
-    }
-    const res = await this.rootStore.agent
-      .getProfile({actor: sess.did})
-      .catch(_e => undefined)
-    if (res?.success) {
-      const updated = {
-        ...sess,
-        handle: res.data.handle,
-        displayName: res.data.displayName,
-        aviUrl: res.data.avatar,
-      }
-      runInAction(() => {
-        this.accounts = [
-          updated,
-          ...this.accounts.filter(
-            account =>
-              !(
-                account.service === updated.service &&
-                account.did === updated.did
-              ),
-          ),
-        ]
-      })
-      await this.rootStore.me.load()
-    }
-  }
-
-  updateLocalAccountData(changes: Partial<AccountData>) {
-    this.accounts = this.accounts.map(acct =>
-      acct.did === this.data?.did ? {...acct, ...changes} : acct,
-    )
-  }
+  async reloadFromServer() {}
 }
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/queries/index.ts b/src/state/queries/index.ts
new file mode 100644
index 000000000..ae3d1595c
--- /dev/null
+++ b/src/state/queries/index.ts
@@ -0,0 +1,5 @@
+import {BskyAgent} from '@atproto/api'
+
+export const PUBLIC_BSKY_AGENT = new BskyAgent({
+  service: 'https://api.bsky.app',
+})
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
new file mode 100644
index 000000000..c2cd19482
--- /dev/null
+++ b/src/state/queries/profile.ts
@@ -0,0 +1,13 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {PUBLIC_BSKY_AGENT} from '#/state/queries'
+
+export function useProfileQuery({did}: {did: string}) {
+  return useQuery({
+    queryKey: ['getProfile', did],
+    queryFn: async () => {
+      const res = await PUBLIC_BSKY_AGENT.getProfile({actor: did})
+      return res.data
+    },
+  })
+}
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 8e1f9c1a1..d0ca10137 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -1,18 +1,25 @@
 import React from 'react'
+import {DeviceEventEmitter} from 'react-native'
 import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api'
 
 import {networkRetry} from '#/lib/async/retry'
 import {logger} from '#/logger'
 import * as persisted from '#/state/persisted'
+import {PUBLIC_BSKY_AGENT} from '#/state/queries'
+import {IS_PROD} from '#/lib/constants'
 
 export type SessionAccount = persisted.PersistedAccount
 
-export type StateContext = {
-  isInitialLoad: boolean
+export type SessionState = {
   agent: BskyAgent
+  isInitialLoad: boolean
+  isSwitchingAccounts: boolean
   accounts: persisted.PersistedAccount[]
   currentAccount: persisted.PersistedAccount | undefined
+}
+export type StateContext = SessionState & {
   hasSession: boolean
+  isSandbox: boolean
 }
 export type ApiContext = {
   createAccount: (props: {
@@ -28,26 +35,26 @@ export type ApiContext = {
     password: string
   }) => Promise<void>
   logout: () => Promise<void>
-  initSession: (account: persisted.PersistedAccount) => Promise<void>
-  resumeSession: (account?: persisted.PersistedAccount) => Promise<void>
-  removeAccount: (
-    account: Partial<Pick<persisted.PersistedAccount, 'handle' | 'did'>>,
-  ) => void
+  initSession: (account: SessionAccount) => Promise<void>
+  resumeSession: (account?: SessionAccount) => Promise<void>
+  removeAccount: (account: SessionAccount) => void
+  selectAccount: (account: SessionAccount) => Promise<void>
   updateCurrentAccount: (
-    account: Pick<persisted.PersistedAccount, 'handle'>,
+    account: Partial<
+      Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
+    >,
   ) => void
+  clearCurrentAccount: () => void
 }
 
-export const PUBLIC_BSKY_AGENT = new BskyAgent({
-  service: 'https://api.bsky.app',
-})
-
 const StateContext = React.createContext<StateContext>({
-  hasSession: false,
+  agent: PUBLIC_BSKY_AGENT,
   isInitialLoad: true,
+  isSwitchingAccounts: false,
   accounts: [],
   currentAccount: undefined,
-  agent: PUBLIC_BSKY_AGENT,
+  hasSession: false,
+  isSandbox: false,
 })
 
 const ApiContext = React.createContext<ApiContext>({
@@ -57,7 +64,9 @@ const ApiContext = React.createContext<ApiContext>({
   initSession: async () => {},
   resumeSession: async () => {},
   removeAccount: () => {},
+  selectAccount: async () => {},
   updateCurrentAccount: () => {},
+  clearCurrentAccount: () => {},
 })
 
 function createPersistSessionHandler(
@@ -73,15 +82,23 @@ 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,
+    )
+
+    if (expired) DeviceEventEmitter.emit('session-dropped')
 
     persistSessionCallback({
       expired,
@@ -91,17 +108,26 @@ function createPersistSessionHandler(
 }
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const [state, setState] = React.useState<StateContext>({
-    hasSession: false,
+  const isDirty = React.useRef(false)
+  const [state, setState] = React.useState<SessionState>({
+    agent: PUBLIC_BSKY_AGENT,
     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 setStateAndPersist = React.useCallback(
+    (fn: (prev: SessionState) => SessionState) => {
+      isDirty.current = true
+      setState(fn)
+    },
+    [setState],
+  )
+
   const upsertAccount = React.useCallback(
     (account: persisted.PersistedAccount, expired = false) => {
-      setState(s => {
+      setStateAndPersist(s => {
         return {
           ...s,
           currentAccount: expired ? undefined : account,
@@ -109,16 +135,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         }
       })
     },
-    [setState],
+    [setStateAndPersist],
   )
 
-  // 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 +165,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 +180,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 +214,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,17 +230,21 @@ 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`)
-    setState(s => {
+    logger.debug(`session: logout`, {}, logger.DebugContext.session)
+    setStateAndPersist(s => {
       return {
         ...s,
         agent: PUBLIC_BSKY_AGENT,
@@ -211,14 +256,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         })),
       }
     })
-  }, [setState])
+  }, [setStateAndPersist])
 
   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,
@@ -265,23 +314,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
   const removeAccount = React.useCallback<ApiContext['removeAccount']>(
     account => {
-      setState(s => {
+      setStateAndPersist(s => {
         return {
           ...s,
-          accounts: s.accounts.filter(
-            a => !(a.did === account.did || a.handle === account.handle),
-          ),
+          accounts: s.accounts.filter(a => a.did !== account.did),
         }
       })
     },
-    [setState],
+    [setStateAndPersist],
   )
 
   const updateCurrentAccount = React.useCallback<
     ApiContext['updateCurrentAccount']
   >(
     account => {
-      setState(s => {
+      setStateAndPersist(s => {
         const currentAccount = s.currentAccount
 
         // ignore, should never happen
@@ -289,52 +336,94 @@ 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],
+    [setStateAndPersist],
+  )
+
+  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(() => {
+    setStateAndPersist(s => ({
+      ...s,
+      currentAccount: undefined,
+    }))
+  }, [setStateAndPersist])
+
   React.useEffect(() => {
-    persisted.write('session', {
-      accounts: state.accounts,
-      currentAccount: state.currentAccount,
-    })
+    if (isDirty.current) {
+      isDirty.current = false
+      persisted.write('session', {
+        accounts: state.accounts,
+        currentAccount: state.currentAccount,
+      })
+    }
   }, [state])
 
   React.useEffect(() => {
     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,
-            },
-            to: {
-              did: session.currentAccount.did,
-              handle: session.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,
+              },
             },
-          })
+            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()
       }
@@ -345,6 +434,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
     () => ({
       ...state,
       hasSession: !!state.currentAccount,
+      isSandbox: state.currentAccount
+        ? !IS_PROD(state.currentAccount?.service)
+        : false,
     }),
     [state],
   )
@@ -357,7 +449,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       initSession,
       resumeSession,
       removeAccount,
+      selectAccount,
       updateCurrentAccount,
+      clearCurrentAccount,
     }),
     [
       createAccount,
@@ -366,7 +460,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..65f9ba26d 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,17 @@ export const CreateAccount = observer(function CreateAccountImpl({
       model.next()
     } else {
       try {
-        await model.submit(onboardingDispatch)
+        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..8c94ef2da 100644
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ b/src/view/com/auth/login/ChooseAccountForm.tsx
@@ -1,52 +1,90 @@
 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 {useProfileQuery} from '#/state/queries/profile'
 
+function AccountItem({
+  account,
+  onSelect,
+}: {
+  account: SessionAccount
+  onSelect: (account: SessionAccount) => void
+}) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {data: profile} = useProfileQuery({did: account.did})
+
+  const onPress = React.useCallback(() => {
+    onSelect(account)
+  }, [account, onSelect])
+
+  return (
+    <TouchableOpacity
+      testID={`chooseAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[pal.view, pal.border, styles.account]}
+      onPress={onPress}
+      accessibilityRole="button"
+      accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
+      accessibilityHint="Double tap to sign in">
+      <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
+        <View style={s.p10}>
+          <UserAvatar avatar={profile?.avatar} size={30} />
+        </View>
+        <Text style={styles.accountText}>
+          <Text type="lg-bold" style={pal.text}>
+            {profile?.displayName || account.handle}{' '}
+          </Text>
+          <Text type="lg" style={[pal.textLight]}>
+            {account.handle}
+          </Text>
+        </Text>
+        <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
+  onSelectAccount: (account?: SessionAccount) => 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 +93,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 +123,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..27d08812c 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -4,7 +4,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
 import {useStores, DEFAULT_SERVICE} from 'state/index'
 import {ServiceDescription} from 'state/models/session'
-import {AccountData} from 'state/models/session'
 import {usePalette} from 'lib/hooks/usePalette'
 import {logger} from '#/logger'
 import {ChooseAccountForm} from './ChooseAccountForm'
@@ -14,6 +13,7 @@ import {SetNewPasswordForm} from './SetNewPasswordForm'
 import {PasswordUpdatedForm} from './PasswordUpdatedForm'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
+import {useSession, SessionAccount} from '#/state/session'
 
 enum Forms {
   Login,
@@ -26,6 +26,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,10 +37,10 @@ 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) => {
+  const onSelectAccount = (account?: SessionAccount) => {
     if (account?.service) {
       setServiceUrl(account.service)
     }
@@ -95,7 +96,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 +114,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 +125,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 +141,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/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index ffae6cbf4..6a846f677 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -23,6 +23,7 @@ import useAppState from 'react-native-appstate-hook'
 import {logger} from '#/logger'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useSession} from '#/state/session'
 
 export const FeedPage = observer(function FeedPageImpl({
   testID,
@@ -38,6 +39,7 @@ export const FeedPage = observer(function FeedPageImpl({
   renderEndOfFeed?: () => JSX.Element
 }) {
   const store = useStores()
+  const {isSandbox} = useSession()
   const pal = usePalette('default')
   const {_} = useLingui()
   const {isDesktop} = useWebMediaQueries()
@@ -140,7 +142,7 @@ export const FeedPage = observer(function FeedPageImpl({
             style={[pal.text, {fontWeight: 'bold'}]}
             text={
               <>
-                {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
+                {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
                 {hasNew && (
                   <View
                     style={{
@@ -173,7 +175,16 @@ export const FeedPage = observer(function FeedPageImpl({
       )
     }
     return <></>
-  }, [isDesktop, pal.view, pal.text, pal.textLight, store, hasNew, _])
+  }, [
+    isDesktop,
+    pal.view,
+    pal.text,
+    pal.textLight,
+    store,
+    hasNew,
+    _,
+    isSandbox,
+  ])
 
   return (
     <View testID={testID} style={s.h100pct}>
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..3481b861c 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,91 @@ 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 {useProfileQuery} from '#/state/queries/profile'
 
 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 {data: profile} = useProfileQuery({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  const onPressSignout = React.useCallback(() => {
+    track('Settings:SignOutButtonClicked')
+    logout()
+  }, [track, logout])
 
-  const store = useStores()
-  const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
+  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 account={account} />
+      )}
+    </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}
+      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 +110,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/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 8c29ad6ab..d79bfe94e 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -19,12 +19,14 @@ import {useLingui} from '@lingui/react'
 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useShellLayout} from '#/state/shell/shell-layout'
+import {useSession} from '#/state/session'
 
 export const FeedsTabBar = observer(function FeedsTabBarImpl(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
   const pal = usePalette('default')
   const store = useStores()
+  const {isSandbox} = useSession()
   const {_} = useLingui()
   const setDrawerOpen = useSetDrawerOpen()
   const items = useHomeTabs(store.preferences.pinnedFeeds)
@@ -59,7 +61,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
           </TouchableOpacity>
         </View>
         <Text style={[brandBlue, s.bold, styles.title]}>
-          {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}
+          {isSandbox ? 'SANDBOX' : 'Bluesky'}
         </Text>
         <View style={[pal.view]}>
           <Link
diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx
index 2f36609e9..489705d10 100644
--- a/src/view/com/testing/TestCtrls.e2e.tsx
+++ b/src/view/com/testing/TestCtrls.e2e.tsx
@@ -3,6 +3,7 @@ import {Pressable, View} from 'react-native'
 import {useStores} from 'state/index'
 import {navigate} from '../../../Navigation'
 import {useModalControls} from '#/state/modals'
+import {useSessionApi} from '#/state/session'
 
 /**
  * This utility component is only included in the test simulator
@@ -14,16 +15,17 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'}
 
 export function TestCtrls() {
   const store = useStores()
+  const {logout, login} = useSessionApi()
   const {openModal} = useModalControls()
   const onPressSignInAlice = async () => {
-    await store.session.login({
+    await login({
       service: 'http://localhost:3000',
       identifier: 'alice.test',
       password: 'hunter2',
     })
   }
   const onPressSignInBob = async () => {
-    await store.session.login({
+    await login({
       service: 'http://localhost:3000',
       identifier: 'bob.test',
       password: 'hunter2',
@@ -45,7 +47,7 @@ export function TestCtrls() {
       />
       <Pressable
         testID="e2eSignOut"
-        onPress={() => store.session.logout()}
+        onPress={() => logout()}
         accessibilityRole="button"
         style={BTN}
       />
diff --git a/src/view/com/util/AccountDropdownBtn.tsx b/src/view/com/util/AccountDropdownBtn.tsx
index 158ed9b6d..96ce678ff 100644
--- a/src/view/com/util/AccountDropdownBtn.tsx
+++ b/src/view/com/util/AccountDropdownBtn.tsx
@@ -8,11 +8,11 @@ import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
 import * as Toast from '../../com/util/Toast'
-import {useSessionApi} from '#/state/session'
+import {useSessionApi, SessionAccount} from '#/state/session'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 
-export function AccountDropdownBtn({handle}: {handle: string}) {
+export function AccountDropdownBtn({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
   const {removeAccount} = useSessionApi()
   const {_} = useLingui()
@@ -21,7 +21,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) {
     {
       label: 'Remove account',
       onPress: () => {
-        removeAccount({handle})
+        removeAccount(account)
         Toast.show('Account removed from quick access')
       },
       icon: {
diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx
index 21f5f7b90..b2375c703 100644
--- a/src/view/com/util/PostSandboxWarning.tsx
+++ b/src/view/com/util/PostSandboxWarning.tsx
@@ -1,13 +1,13 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {Text} from './text/Text'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useSession} from '#/state/session'
 
 export function PostSandboxWarning() {
-  const store = useStores()
+  const {isSandbox} = useSession()
   const pal = usePalette('default')
-  if (store.session.isSandbox) {
+  if (isSandbox) {
     return (
       <View style={styles.container}>
         <Text
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 062533c27..e56a50d79 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 {useProfileQuery} from '#/state/queries/profile'
 
 // TEMPORARY (APP-700)
 // remove after backend testing finishes
@@ -67,6 +68,70 @@ 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 {data: profile} = useProfileQuery({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  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}>
+          {profile?.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 ${profile?.displayName} out of Bluesky`}>
+          <Text type="lg" style={pal.link}>
+            Sign out
+          </Text>
+        </TouchableOpacity>
+      ) : (
+        <AccountDropdownBtn account={account} />
+      )}
+    </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 +147,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 +183,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 +214,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 +271,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 +280,7 @@ export const SettingsScreen = withAuthRequired(
                 <Text type="lg-medium" style={pal.text}>
                   Email:{' '}
                 </Text>
-                {!store.session.emailNeedsConfirmation && (
+                {currentAccount.emailConfirmed && (
                   <>
                     <FontAwesomeIcon
                       icon="check"
@@ -236,7 +290,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 +309,8 @@ export const SettingsScreen = withAuthRequired(
                 </Link>
               </View>
               <View style={styles.spacer20} />
-              <EmailConfirmationNotice />
+
+              {!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
             </>
           ) : null}
           <View style={[s.flexRow, styles.heading]}>
@@ -264,70 +319,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 +363,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 +445,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 +466,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 +488,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 +505,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 +526,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 +553,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 +574,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 +703,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/Drawer.tsx b/src/view/shell/Drawer.tsx
index 99e1d7d98..609348e4d 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -47,6 +47,57 @@ import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useSetDrawerOpen} from '#/state/shell'
 import {useModalControls} from '#/state/modals'
+import {useSession, SessionAccount} from '#/state/session'
+import {useProfileQuery} from '#/state/queries/profile'
+
+export function DrawerProfileCard({
+  account,
+  onPressProfile,
+}: {
+  account: SessionAccount
+  onPressProfile: () => void
+}) {
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {data: profile} = useProfileQuery({did: account.did})
+
+  return (
+    <TouchableOpacity
+      testID="profileCardButton"
+      accessibilityLabel={_(msg`Profile`)}
+      accessibilityHint="Navigates to your profile"
+      onPress={onPressProfile}>
+      <UserAvatar
+        size={80}
+        avatar={profile?.avatar}
+        // See https://github.com/bluesky-social/social-app/pull/1801:
+        usePlainRNImage={true}
+      />
+      <Text
+        type="title-lg"
+        style={[pal.text, s.bold, styles.profileCardDisplayName]}
+        numberOfLines={1}>
+        {profile?.displayName || account.handle}
+      </Text>
+      <Text
+        type="2xl"
+        style={[pal.textLight, styles.profileCardHandle]}
+        numberOfLines={1}>
+        @{account.handle}
+      </Text>
+      <Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
+        <Text type="xl-medium" style={pal.text}>
+          {formatCountShortOnly(profile?.followersCount ?? 0)}
+        </Text>{' '}
+        {pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '}
+        <Text type="xl-medium" style={pal.text}>
+          {formatCountShortOnly(profile?.followsCount ?? 0)}
+        </Text>{' '}
+        following
+      </Text>
+    </TouchableOpacity>
+  )
+}
 
 export const DrawerContent = observer(function DrawerContentImpl() {
   const theme = useTheme()
@@ -58,6 +109,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
   const {track} = useAnalytics()
   const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
     useNavigationTabState()
+  const {currentAccount} = useSession()
 
   const {notifications} = store.me
 
@@ -135,11 +187,11 @@ export const DrawerContent = observer(function DrawerContentImpl() {
     track('Menu:FeedbackClicked')
     Linking.openURL(
       FEEDBACK_FORM_URL({
-        email: store.session.currentSession?.email,
-        handle: store.session.currentSession?.handle,
+        email: currentAccount?.email,
+        handle: currentAccount?.handle,
       }),
     )
-  }, [track, store.session.currentSession])
+  }, [track, currentAccount])
 
   const onPressHelp = React.useCallback(() => {
     track('Menu:HelpClicked')
@@ -159,42 +211,12 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       <SafeAreaView style={s.flex1}>
         <ScrollView style={styles.main}>
           <View style={{}}>
-            <TouchableOpacity
-              testID="profileCardButton"
-              accessibilityLabel={_(msg`Profile`)}
-              accessibilityHint="Navigates to your profile"
-              onPress={onPressProfile}>
-              <UserAvatar
-                size={80}
-                avatar={store.me.avatar}
-                // See https://github.com/bluesky-social/social-app/pull/1801:
-                usePlainRNImage={true}
+            {currentAccount && (
+              <DrawerProfileCard
+                account={currentAccount}
+                onPressProfile={onPressProfile}
               />
-              <Text
-                type="title-lg"
-                style={[pal.text, s.bold, styles.profileCardDisplayName]}
-                numberOfLines={1}>
-                {store.me.displayName || store.me.handle}
-              </Text>
-              <Text
-                type="2xl"
-                style={[pal.textLight, styles.profileCardHandle]}
-                numberOfLines={1}>
-                @{store.me.handle}
-              </Text>
-              <Text
-                type="xl"
-                style={[pal.textLight, styles.profileCardFollowers]}>
-                <Text type="xl-medium" style={pal.text}>
-                  {formatCountShortOnly(store.me.followersCount ?? 0)}
-                </Text>{' '}
-                {pluralize(store.me.followersCount || 0, 'follower')} &middot;{' '}
-                <Text type="xl-medium" style={pal.text}>
-                  {formatCountShortOnly(store.me.followsCount ?? 0)}
-                </Text>{' '}
-                following
-              </Text>
-            </TouchableOpacity>
+            )}
           </View>
 
           <InviteCodes style={{paddingLeft: 0}} />
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index b85823b6f..0586323b4 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -41,18 +41,25 @@ import {router} from '../../../routes'
 import {makeProfileLink} from 'lib/routes/links'
 import {useLingui} from '@lingui/react'
 import {Trans, msg} from '@lingui/macro'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
 
 const ProfileCard = observer(function ProfileCardImpl() {
-  const store = useStores()
+  const {currentAccount} = useSession()
+  const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did})
   const {isDesktop} = useWebMediaQueries()
   const size = 48
-  return store.me.handle ? (
+
+  return !isLoading && profile ? (
     <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 +262,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         pal.view,
         pal.border,
       ]}>
-      {store.session.hasSession && <ProfileCard />}
+      <ProfileCard />
       <BackBtn />
       <NavItem
         href="/"
@@ -360,26 +367,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 +403,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..98f54c7ed 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -14,11 +14,12 @@ 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 {isSandbox, hasSession, currentAccount} = useSession()
 
   const {isTablet} = useWebMediaQueries()
   if (isTablet) {
@@ -27,10 +28,10 @@ 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 ? (
+        {isSandbox ? (
           <View style={[palError.view, styles.messageLine, s.p10]}>
             <Text type="md" style={[palError.text, s.bold]}>
               SANDBOX. Posts and accounts are not permanent.
@@ -42,8 +43,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}>