about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/auth/LoggedOut.tsx7
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx10
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx113
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx2
-rw-r--r--src/view/com/auth/login/Login.tsx8
-rw-r--r--src/view/com/auth/login/LoginForm.tsx8
-rw-r--r--src/view/com/auth/login/SetNewPasswordForm.tsx2
-rw-r--r--src/view/com/auth/withAuthRequired.tsx8
-rw-r--r--src/view/com/modals/ChangeEmail.tsx25
-rw-r--r--src/view/com/modals/SwitchAccount.tsx147
-rw-r--r--src/view/com/modals/VerifyEmail.tsx20
11 files changed, 187 insertions, 163 deletions
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 3e2c9c1bf..0d8172964 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -6,7 +6,6 @@ import {CreateAccount} from 'view/com/auth/create/CreateAccount'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {SplashScreen} from './SplashScreen'
 import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
@@ -19,7 +18,6 @@ enum ScreenState {
 
 export const LoggedOut = observer(function LoggedOutImpl() {
   const pal = usePalette('default')
-  const store = useStores()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {screen} = useAnalytics()
   const [screenState, setScreenState] = React.useState<ScreenState>(
@@ -31,10 +29,7 @@ export const LoggedOut = observer(function LoggedOutImpl() {
     setMinimalShellMode(true)
   }, [screen, setMinimalShellMode])
 
-  if (
-    store.session.isResumingSession ||
-    screenState === ScreenState.S_LoginOrCreateAccount
-  ) {
+  if (screenState === ScreenState.S_LoginOrCreateAccount) {
     return (
       <SplashScreen
         onPressSignin={() => setScreenState(ScreenState.S_Login)}
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 8e2bbed85..0f56755df 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -18,6 +18,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useOnboardingDispatch} from '#/state/shell'
+import {useSessionApi} from '#/state/session'
 
 import {Step1} from './Step1'
 import {Step2} from './Step2'
@@ -34,6 +35,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
   const model = React.useMemo(() => new CreateAccountModel(store), [store])
   const {_} = useLingui()
   const onboardingDispatch = useOnboardingDispatch()
+  const {createAccount} = useSessionApi()
 
   React.useEffect(() => {
     screen('CreateAccount')
@@ -64,14 +66,18 @@ export const CreateAccount = observer(function CreateAccountImpl({
       model.next()
     } else {
       try {
-        await model.submit(onboardingDispatch)
+        console.log('BEFORE')
+        await model.submit({
+          onboardingDispatch,
+          createAccount,
+        })
       } catch {
         // dont need to handle here
       } finally {
         track('Try Create Account')
       }
     }
-  }, [model, track, onboardingDispatch])
+  }, [model, track, onboardingDispatch, createAccount])
 
   return (
     <LoggedOutLayout
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx
index 596a8e411..38c13ba09 100644
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ b/src/view/com/auth/login/ChooseAccountForm.tsx
@@ -1,52 +1,93 @@
 import React from 'react'
-import {
-  ActivityIndicator,
-  ScrollView,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {ScrollView, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {UserAvatar} from '../../util/UserAvatar'
 import {s} from 'lib/styles'
-import {RootStoreModel} from 'state/index'
 import {AccountData} from 'state/models/session'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {styles} from './styles'
+import {useSession, useSessionApi, SessionAccount} from '#/state/session'
+import {useGetProfile} from '#/data/useGetProfile'
 
+function AccountItem({
+  account,
+  onSelect,
+}: {
+  account: SessionAccount
+  onSelect: (account: SessionAccount) => void
+}) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {isError, data} = useGetProfile({did: account.did})
+
+  const onPress = React.useCallback(() => {
+    onSelect(account)
+  }, [account, onSelect])
+
+  if (isError) return null
+
+  return (
+    <TouchableOpacity
+      testID={`chooseAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[pal.view, pal.border, styles.account]}
+      onPress={onPress}
+      accessibilityRole="button"
+      accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
+      accessibilityHint="Double tap to sign in">
+      <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
+        <View style={s.p10}>
+          <UserAvatar avatar={data?.avatar} size={30} />
+        </View>
+        <Text style={styles.accountText}>
+          <Text type="lg-bold" style={pal.text}>
+            {data?.displayName || account.handle}{' '}
+          </Text>
+          <Text type="lg" style={[pal.textLight]}>
+            {account.handle}
+          </Text>
+        </Text>
+        <FontAwesomeIcon
+          icon="angle-right"
+          size={16}
+          style={[pal.text, s.mr10]}
+        />
+      </View>
+    </TouchableOpacity>
+  )
+}
 export const ChooseAccountForm = ({
-  store,
   onSelectAccount,
   onPressBack,
 }: {
-  store: RootStoreModel
   onSelectAccount: (account?: AccountData) => void
   onPressBack: () => void
 }) => {
   const {track, screen} = useAnalytics()
   const pal = usePalette('default')
-  const [isProcessing, setIsProcessing] = React.useState(false)
   const {_} = useLingui()
+  const {accounts} = useSession()
+  const {initSession} = useSessionApi()
 
   React.useEffect(() => {
     screen('Choose Account')
   }, [screen])
 
-  const onTryAccount = async (account: AccountData) => {
-    if (account.accessJwt && account.refreshJwt) {
-      setIsProcessing(true)
-      if (await store.session.resumeSession(account)) {
+  const onSelect = React.useCallback(
+    async (account: SessionAccount) => {
+      if (account.accessJwt) {
+        await initSession(account)
         track('Sign In', {resumedSession: true})
-        setIsProcessing(false)
-        return
+      } else {
+        onSelectAccount(account)
       }
-      setIsProcessing(false)
-    }
-    onSelectAccount(account)
-  }
+    },
+    [track, initSession, onSelectAccount],
+  )
 
   return (
     <ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
@@ -55,35 +96,8 @@ export const ChooseAccountForm = ({
         style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
         <Trans>Sign in as...</Trans>
       </Text>
-      {store.session.accounts.map(account => (
-        <TouchableOpacity
-          testID={`chooseAccountBtn-${account.handle}`}
-          key={account.did}
-          style={[pal.view, pal.border, styles.account]}
-          onPress={() => onTryAccount(account)}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
-          accessibilityHint="Double tap to sign in">
-          <View
-            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-            <View style={s.p10}>
-              <UserAvatar avatar={account.aviUrl} size={30} />
-            </View>
-            <Text style={styles.accountText}>
-              <Text type="lg-bold" style={pal.text}>
-                {account.displayName || account.handle}{' '}
-              </Text>
-              <Text type="lg" style={[pal.textLight]}>
-                {account.handle}
-              </Text>
-            </Text>
-            <FontAwesomeIcon
-              icon="angle-right"
-              size={16}
-              style={[pal.text, s.mr10]}
-            />
-          </View>
-        </TouchableOpacity>
+      {accounts.map(account => (
+        <AccountItem key={account.did} account={account} onSelect={onSelect} />
       ))}
       <TouchableOpacity
         testID="chooseNewAccountBtn"
@@ -112,7 +126,6 @@ export const ChooseAccountForm = ({
           </Text>
         </TouchableOpacity>
         <View style={s.flex1} />
-        {isProcessing && <ActivityIndicator />}
       </View>
     </ScrollView>
   )
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
index 9bfab18b5..a794665c9 100644
--- a/src/view/com/auth/login/ForgotPasswordForm.tsx
+++ b/src/view/com/auth/login/ForgotPasswordForm.tsx
@@ -15,7 +15,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
 import {toNiceDomain} from 'lib/strings/url-helpers'
-import {RootStoreModel} from 'state/index'
 import {ServiceDescription} from 'state/models/session'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -36,7 +35,6 @@ export const ForgotPasswordForm = ({
   onPressBack,
   onEmailSent,
 }: {
-  store: RootStoreModel
   error: string
   serviceUrl: string
   serviceDescription: ServiceDescription | undefined
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
index 401b7d980..de00d6ca2 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -14,6 +14,7 @@ import {SetNewPasswordForm} from './SetNewPasswordForm'
 import {PasswordUpdatedForm} from './PasswordUpdatedForm'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
+import {useSession} from '#/state/session'
 
 enum Forms {
   Login,
@@ -26,6 +27,7 @@ enum Forms {
 export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   const pal = usePalette('default')
   const store = useStores()
+  const {accounts} = useSession()
   const {track} = useAnalytics()
   const {_} = useLingui()
   const [error, setError] = useState<string>('')
@@ -36,7 +38,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   >(undefined)
   const [initialHandle, setInitialHandle] = useState<string>('')
   const [currentForm, setCurrentForm] = useState<Forms>(
-    store.session.hasAccounts ? Forms.ChooseAccount : Forms.Login,
+    accounts.length ? Forms.ChooseAccount : Forms.Login,
   )
 
   const onSelectAccount = (account?: AccountData) => {
@@ -95,7 +97,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Sign in`)}
           description={_(msg`Enter your username and password`)}>
           <LoginForm
-            store={store}
             error={error}
             serviceUrl={serviceUrl}
             serviceDescription={serviceDescription}
@@ -114,7 +115,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Sign in as...`)}
           description={_(msg`Select from an existing account`)}>
           <ChooseAccountForm
-            store={store}
             onSelectAccount={onSelectAccount}
             onPressBack={onPressBack}
           />
@@ -126,7 +126,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Forgot Password`)}
           description={_(msg`Let's get your password reset!`)}>
           <ForgotPasswordForm
-            store={store}
             error={error}
             serviceUrl={serviceUrl}
             serviceDescription={serviceDescription}
@@ -143,7 +142,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
           title={_(msg`Forgot Password`)}
           description={_(msg`Let's get your password reset!`)}>
           <SetNewPasswordForm
-            store={store}
             error={error}
             serviceUrl={serviceUrl}
             setError={setError}
diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx
index 166a7cbd8..be3a95131 100644
--- a/src/view/com/auth/login/LoginForm.tsx
+++ b/src/view/com/auth/login/LoginForm.tsx
@@ -15,7 +15,6 @@ import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
 import {createFullHandle} from 'lib/strings/handles'
 import {toNiceDomain} from 'lib/strings/url-helpers'
-import {RootStoreModel} from 'state/index'
 import {ServiceDescription} from 'state/models/session'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -29,7 +28,6 @@ import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
 
 export const LoginForm = ({
-  store,
   error,
   serviceUrl,
   serviceDescription,
@@ -40,7 +38,6 @@ export const LoginForm = ({
   onPressBack,
   onPressForgotPassword,
 }: {
-  store: RootStoreModel
   error: string
   serviceUrl: string
   serviceDescription: ServiceDescription | undefined
@@ -106,11 +103,6 @@ export const LoginForm = ({
         identifier: fullIdent,
         password,
       })
-      await store.session.login({
-        service: serviceUrl,
-        identifier: fullIdent,
-        password,
-      })
     } catch (e: any) {
       const errMsg = e.toString()
       logger.warn('Failed to login', {error: e})
diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx
index 04eaa2842..2bb614df2 100644
--- a/src/view/com/auth/login/SetNewPasswordForm.tsx
+++ b/src/view/com/auth/login/SetNewPasswordForm.tsx
@@ -10,7 +10,6 @@ import {BskyAgent} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
-import {RootStoreModel} from 'state/index'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
@@ -27,7 +26,6 @@ export const SetNewPasswordForm = ({
   onPressBack,
   onPasswordSet,
 }: {
-  store: RootStoreModel
   error: string
   serviceUrl: string
   setError: (v: string) => void
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
index 898f81051..4b8b31d6c 100644
--- a/src/view/com/auth/withAuthRequired.tsx
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -6,7 +6,6 @@ import {
   TouchableOpacity,
 } from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
 import {CenteredView} from '../util/Views'
 import {LoggedOut} from './LoggedOut'
 import {Onboarding} from './Onboarding'
@@ -14,17 +13,18 @@ import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {STATUS_PAGE_URL} from 'lib/constants'
 import {useOnboardingState} from '#/state/shell'
+import {useSession} from '#/state/session'
 
 export const withAuthRequired = <P extends object>(
   Component: React.ComponentType<P>,
 ): React.FC<P> =>
   observer(function AuthRequired(props: P) {
-    const store = useStores()
+    const {isInitialLoad, hasSession} = useSession()
     const onboardingState = useOnboardingState()
-    if (store.session.isResumingSession) {
+    if (isInitialLoad) {
       return <Loading />
     }
-    if (!store.session.hasSession) {
+    if (!hasSession) {
       return <LoggedOut />
     }
     if (onboardingState.isActive) {
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx
index 710c0588e..6f7a92102 100644
--- a/src/view/com/modals/ChangeEmail.tsx
+++ b/src/view/com/modals/ChangeEmail.tsx
@@ -6,7 +6,6 @@ import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
@@ -15,6 +14,7 @@ import {cleanError} from 'lib/strings/errors'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi} from '#/state/session'
 
 enum Stages {
   InputEmail,
@@ -26,12 +26,11 @@ export const snapPoints = ['90%']
 
 export const Component = observer(function Component({}: {}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {agent, currentAccount} = useSession()
+  const {updateCurrentAccount} = useSessionApi()
   const {_} = useLingui()
   const [stage, setStage] = useState<Stages>(Stages.InputEmail)
-  const [email, setEmail] = useState<string>(
-    store.session.currentSession?.email || '',
-  )
+  const [email, setEmail] = useState<string>(currentAccount?.email || '')
   const [confirmationCode, setConfirmationCode] = useState<string>('')
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
@@ -39,19 +38,19 @@ export const Component = observer(function Component({}: {}) {
   const {openModal, closeModal} = useModalControls()
 
   const onRequestChange = async () => {
-    if (email === store.session.currentSession?.email) {
+    if (email === currentAccount?.email) {
       setError('Enter your new email above')
       return
     }
     setError('')
     setIsProcessing(true)
     try {
-      const res = await store.agent.com.atproto.server.requestEmailUpdate()
+      const res = await agent.com.atproto.server.requestEmailUpdate()
       if (res.data.tokenRequired) {
         setStage(Stages.ConfirmCode)
       } else {
-        await store.agent.com.atproto.server.updateEmail({email: email.trim()})
-        store.session.updateLocalAccountData({
+        await agent.com.atproto.server.updateEmail({email: email.trim()})
+        updateCurrentAccount({
           email: email.trim(),
           emailConfirmed: false,
         })
@@ -79,11 +78,11 @@ export const Component = observer(function Component({}: {}) {
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.updateEmail({
+      await agent.com.atproto.server.updateEmail({
         email: email.trim(),
         token: confirmationCode.trim(),
       })
-      store.session.updateLocalAccountData({
+      updateCurrentAccount({
         email: email.trim(),
         emailConfirmed: false,
       })
@@ -120,8 +119,8 @@ export const Component = observer(function Component({}: {}) {
           ) : stage === Stages.ConfirmCode ? (
             <Trans>
               An email has been sent to your previous address,{' '}
-              {store.session.currentSession?.email || ''}. It includes a
-              confirmation code which you can enter below.
+              {currentAccount?.email || ''}. It includes a confirmation code
+              which you can enter below.
             </Trans>
           ) : (
             <Trans>
diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx
index 1d9457995..2ff70eea4 100644
--- a/src/view/com/modals/SwitchAccount.tsx
+++ b/src/view/com/modals/SwitchAccount.tsx
@@ -6,7 +6,6 @@ import {
   View,
 } from 'react-native'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
@@ -19,26 +18,94 @@ import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
 import {Haptics} from 'lib/haptics'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useSession, useSessionApi, SessionAccount} from '#/state/session'
+import {useGetProfile} from '#/data/useGetProfile'
 
 export const snapPoints = ['40%', '90%']
 
-export function Component({}: {}) {
+function SwitchAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {track} = useAnalytics()
-  const {_: _lingui} = useLingui()
+  const {isSwitchingAccounts, currentAccount} = useSession()
+  const {logout} = useSessionApi()
+  const {isError, data: profile} = useGetProfile({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  const onPressSignout = React.useCallback(() => {
+    track('Settings:SignOutButtonClicked')
+    logout()
+  }, [track, logout])
+
+  // TODO
+  if (isError || !currentAccount) return null
+
+  const contents = (
+    <View style={[pal.view, styles.linkCard]}>
+      <View style={styles.avi}>
+        <UserAvatar size={40} avatar={profile?.avatar} />
+      </View>
+      <View style={[s.flex1]}>
+        <Text type="md-bold" style={pal.text} numberOfLines={1}>
+          {profile?.displayName || currentAccount.handle}
+        </Text>
+        <Text type="sm" style={pal.textLight} numberOfLines={1}>
+          {currentAccount.handle}
+        </Text>
+      </View>
+
+      {isCurrentAccount ? (
+        <TouchableOpacity
+          testID="signOutBtn"
+          onPress={isSwitchingAccounts ? undefined : onPressSignout}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Sign out`)}
+          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
+          <Text type="lg" style={pal.link}>
+            <Trans>Sign out</Trans>
+          </Text>
+        </TouchableOpacity>
+      ) : (
+        <AccountDropdownBtn handle={account.handle} />
+      )}
+    </View>
+  )
 
-  const store = useStores()
-  const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
+  return isCurrentAccount ? (
+    <Link
+      href={makeProfileLink({
+        did: currentAccount.did,
+        handle: currentAccount.handle,
+      })}
+      title="Your profile"
+      noFeedback>
+      {contents}
+    </Link>
+  ) : (
+    <TouchableOpacity
+      testID={`switchToAccountBtn-${account.handle}`}
+      key={account.did}
+      style={[isSwitchingAccounts && styles.dimmed]}
+      onPress={
+        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+      }
+      accessibilityRole="button"
+      accessibilityLabel={`Switch to ${account.handle}`}
+      accessibilityHint="Switches the account you are logged in to">
+      {contents}
+    </TouchableOpacity>
+  )
+}
+
+export function Component({}: {}) {
+  const pal = usePalette('default')
+  const {isSwitchingAccounts, currentAccount, accounts} = useSession()
 
   React.useEffect(() => {
     Haptics.default()
   })
 
-  const onPressSignout = React.useCallback(() => {
-    track('Settings:SignOutButtonClicked')
-    store.session.logout()
-  }, [track, store])
-
   return (
     <BottomSheetScrollView
       style={[styles.container, pal.view]}
@@ -46,62 +113,20 @@ export function Component({}: {}) {
       <Text type="title-xl" style={[styles.title, pal.text]}>
         <Trans>Switch Account</Trans>
       </Text>
-      {isSwitching ? (
+
+      {isSwitchingAccounts || !currentAccount ? (
         <View style={[pal.view, styles.linkCard]}>
           <ActivityIndicator />
         </View>
       ) : (
-        <Link href={makeProfileLink(store.me)} title="Your profile" noFeedback>
-          <View style={[pal.view, styles.linkCard]}>
-            <View style={styles.avi}>
-              <UserAvatar size={40} avatar={store.me.avatar} />
-            </View>
-            <View style={[s.flex1]}>
-              <Text type="md-bold" style={pal.text} numberOfLines={1}>
-                {store.me.displayName || store.me.handle}
-              </Text>
-              <Text type="sm" style={pal.textLight} numberOfLines={1}>
-                {store.me.handle}
-              </Text>
-            </View>
-            <TouchableOpacity
-              testID="signOutBtn"
-              onPress={isSwitching ? undefined : onPressSignout}
-              accessibilityRole="button"
-              accessibilityLabel={_lingui(msg`Sign out`)}
-              accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
-              <Text type="lg" style={pal.link}>
-                <Trans>Sign out</Trans>
-              </Text>
-            </TouchableOpacity>
-          </View>
-        </Link>
+        <SwitchAccountCard account={currentAccount} />
       )}
-      {store.session.switchableAccounts.map(account => (
-        <TouchableOpacity
-          testID={`switchToAccountBtn-${account.handle}`}
-          key={account.did}
-          style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
-          onPress={
-            isSwitching ? undefined : () => onPressSwitchAccount(account)
-          }
-          accessibilityRole="button"
-          accessibilityLabel={`Switch to ${account.handle}`}
-          accessibilityHint="Switches the account you are logged in to">
-          <View style={styles.avi}>
-            <UserAvatar size={40} avatar={account.aviUrl} />
-          </View>
-          <View style={[s.flex1]}>
-            <Text type="md-bold" style={pal.text}>
-              {account.displayName || account.handle}
-            </Text>
-            <Text type="sm" style={pal.textLight}>
-              {account.handle}
-            </Text>
-          </View>
-          <AccountDropdownBtn handle={account.handle} />
-        </TouchableOpacity>
-      ))}
+
+      {accounts
+        .filter(a => a.did !== currentAccount?.did)
+        .map(account => (
+          <SwitchAccountCard key={account.did} account={account} />
+        ))}
     </BottomSheetScrollView>
   )
 }
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index e48e0e4a2..106e05b87 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -14,7 +14,6 @@ import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import * as Toast from '../util/Toast'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
@@ -23,6 +22,7 @@ import {cleanError} from 'lib/strings/errors'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useSession, useSessionApi} from '#/state/session'
 
 export const snapPoints = ['90%']
 
@@ -38,7 +38,8 @@ export const Component = observer(function Component({
   showReminder?: boolean
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {agent, currentAccount} = useSession()
+  const {updateCurrentAccount} = useSessionApi()
   const {_} = useLingui()
   const [stage, setStage] = useState<Stages>(
     showReminder ? Stages.Reminder : Stages.Email,
@@ -53,7 +54,7 @@ export const Component = observer(function Component({
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.requestEmailConfirmation()
+      await agent.com.atproto.server.requestEmailConfirmation()
       setStage(Stages.ConfirmCode)
     } catch (e) {
       setError(cleanError(String(e)))
@@ -66,11 +67,11 @@ export const Component = observer(function Component({
     setError('')
     setIsProcessing(true)
     try {
-      await store.agent.com.atproto.server.confirmEmail({
-        email: (store.session.currentSession?.email || '').trim(),
+      await agent.com.atproto.server.confirmEmail({
+        email: (currentAccount?.email || '').trim(),
         token: confirmationCode.trim(),
       })
-      store.session.updateLocalAccountData({emailConfirmed: true})
+      updateCurrentAccount({emailConfirmed: true})
       Toast.show('Email verified')
       closeModal()
     } catch (e) {
@@ -112,9 +113,8 @@ export const Component = observer(function Component({
             </Trans>
           ) : stage === Stages.ConfirmCode ? (
             <Trans>
-              An email has been sent to{' '}
-              {store.session.currentSession?.email || ''}. It includes a
-              confirmation code which you can enter below.
+              An email has been sent to {currentAccount?.email || ''}. It
+              includes a confirmation code which you can enter below.
             </Trans>
           ) : (
             ''
@@ -130,7 +130,7 @@ export const Component = observer(function Component({
                 size={16}
               />
               <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}>
-                {store.session.currentSession?.email || ''}
+                {currentAccount?.email || ''}
               </Text>
             </View>
             <Pressable