about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/models/session.ts4
-rw-r--r--src/view/com/auth/CreateAccount.tsx (renamed from src/view/com/login/CreateAccount.tsx)0
-rw-r--r--src/view/com/auth/LoggedOut.tsx67
-rw-r--r--src/view/com/auth/Logo.tsx (renamed from src/view/com/login/Logo.tsx)18
-rw-r--r--src/view/com/auth/Signin.tsx (renamed from src/view/com/login/Signin.tsx)0
-rw-r--r--src/view/com/auth/SplashScreen.tsx92
-rw-r--r--src/view/com/auth/SplashScreen.web.tsx102
-rw-r--r--src/view/com/auth/withAuthRequired.tsx47
-rw-r--r--src/view/screens/Home.tsx169
-rw-r--r--src/view/screens/Login.tsx164
-rw-r--r--src/view/screens/Login.web.tsx156
-rw-r--r--src/view/screens/Notifications.tsx140
-rw-r--r--src/view/screens/PostRepostedBy.tsx5
-rw-r--r--src/view/screens/PostThread.tsx5
-rw-r--r--src/view/screens/PostUpvotedBy.tsx5
-rw-r--r--src/view/screens/PrivacyPolicy.tsx2
-rw-r--r--src/view/screens/Profile.tsx317
-rw-r--r--src/view/screens/ProfileFollowers.tsx5
-rw-r--r--src/view/screens/ProfileFollows.tsx5
-rw-r--r--src/view/screens/Search.tsx285
-rw-r--r--src/view/screens/Search.web.tsx73
-rw-r--r--src/view/screens/Settings.tsx399
-rw-r--r--src/view/shell/desktop/LeftNav.tsx22
-rw-r--r--src/view/shell/desktop/RightNav.tsx4
-rw-r--r--src/view/shell/index.tsx15
-rw-r--r--src/view/shell/index.web.tsx12
26 files changed, 1049 insertions, 1064 deletions
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index b79283be1..7c4d0931c 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -122,7 +122,9 @@ export class SessionModel {
       try {
         return await this.resumeSession(sess)
       } finally {
-        this.isResumingSession = false
+        runInAction(() => {
+          this.isResumingSession = false
+        })
       }
     } else {
       this.rootStore.log.debug(
diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/auth/CreateAccount.tsx
index a24dc4e35..a24dc4e35 100644
--- a/src/view/com/login/CreateAccount.tsx
+++ b/src/view/com/auth/CreateAccount.tsx
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
new file mode 100644
index 000000000..47dd51d9c
--- /dev/null
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import {SafeAreaView} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {Signin} from 'view/com/auth/Signin'
+import {CreateAccount} from 'view/com/auth/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'
+import {SplashScreen} from './SplashScreen'
+import {CenteredView} from '../util/Views'
+
+enum ScreenState {
+  S_SigninOrCreateAccount,
+  S_Signin,
+  S_CreateAccount,
+}
+
+export const LoggedOut = observer(() => {
+  const pal = usePalette('default')
+  const store = useStores()
+  const {screen} = useAnalytics()
+  const [screenState, setScreenState] = React.useState<ScreenState>(
+    ScreenState.S_SigninOrCreateAccount,
+  )
+
+  React.useEffect(() => {
+    screen('Login')
+    store.shell.setMinimalShellMode(true)
+  }, [store, screen])
+
+  if (
+    store.session.isResumingSession ||
+    screenState === ScreenState.S_SigninOrCreateAccount
+  ) {
+    return (
+      <SplashScreen
+        onPressSignin={() => setScreenState(ScreenState.S_Signin)}
+        onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
+      />
+    )
+  }
+
+  return (
+    <CenteredView style={[s.hContentRegion, pal.view]}>
+      <SafeAreaView testID="noSessionView" style={s.hContentRegion}>
+        <ErrorBoundary>
+          {screenState === ScreenState.S_Signin ? (
+            <Signin
+              onPressBack={() =>
+                setScreenState(ScreenState.S_SigninOrCreateAccount)
+              }
+            />
+          ) : undefined}
+          {screenState === ScreenState.S_CreateAccount ? (
+            <CreateAccount
+              onPressBack={() =>
+                setScreenState(ScreenState.S_SigninOrCreateAccount)
+              }
+            />
+          ) : undefined}
+        </ErrorBoundary>
+      </SafeAreaView>
+    </CenteredView>
+  )
+})
diff --git a/src/view/com/login/Logo.tsx b/src/view/com/auth/Logo.tsx
index 7601ea31f..ac408cd2f 100644
--- a/src/view/com/login/Logo.tsx
+++ b/src/view/com/auth/Logo.tsx
@@ -1,28 +1,19 @@
 import React from 'react'
-import {StyleSheet} from 'react-native'
-import LinearGradient from 'react-native-linear-gradient'
-import {s, gradients} from 'lib/styles'
+import {StyleSheet, View} from 'react-native'
+import {s, colors} from 'lib/styles'
 import {Text} from '../util/text/Text'
 
 export const LogoTextHero = () => {
   return (
-    <LinearGradient
-      colors={[gradients.blue.start, gradients.blue.end]}
-      start={{x: 0, y: 0}}
-      end={{x: 1, y: 1}}
-      style={[styles.textHero]}>
+    <View style={[styles.textHero]}>
       <Text type="title-lg" style={[s.white, s.bold]}>
         Bluesky
       </Text>
-    </LinearGradient>
+    </View>
   )
 }
 
 const styles = StyleSheet.create({
-  logo: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-  },
   textHero: {
     flexDirection: 'row',
     alignItems: 'center',
@@ -30,5 +21,6 @@ const styles = StyleSheet.create({
     paddingRight: 20,
     paddingVertical: 15,
     marginBottom: 20,
+    backgroundColor: colors.blue3,
   },
 })
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/auth/Signin.tsx
index 6faf5ff12..6faf5ff12 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/auth/Signin.tsx
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
new file mode 100644
index 000000000..27943f64d
--- /dev/null
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -0,0 +1,92 @@
+import React from 'react'
+import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import Image, {Source as ImageSource} from 'view/com/util/images/Image'
+import {Text} from 'view/com/util/text/Text'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
+import {colors} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {CLOUD_SPLASH} from 'lib/assets'
+import {CenteredView} from '../util/Views'
+
+export const SplashScreen = ({
+  onPressSignin,
+  onPressCreateAccount,
+}: {
+  onPressSignin: () => void
+  onPressCreateAccount: () => void
+}) => {
+  const pal = usePalette('default')
+  return (
+    <CenteredView style={styles.container}>
+      <Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
+      <SafeAreaView testID="noSessionView" style={styles.container}>
+        <ErrorBoundary>
+          <View style={styles.hero}>
+            <View style={styles.heroText}>
+              <Text style={styles.title}>Bluesky</Text>
+            </View>
+          </View>
+          <View testID="signinOrCreateAccount" style={styles.btns}>
+            <TouchableOpacity
+              testID="createAccountButton"
+              style={[pal.view, styles.btn]}
+              onPress={onPressCreateAccount}>
+              <Text style={[pal.link, styles.btnLabel]}>
+                Create a new account
+              </Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              testID="signInButton"
+              style={[pal.view, styles.btn]}
+              onPress={onPressSignin}>
+              <Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
+            </TouchableOpacity>
+          </View>
+        </ErrorBoundary>
+      </SafeAreaView>
+    </CenteredView>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    height: '100%',
+  },
+  hero: {
+    flex: 2,
+    justifyContent: 'center',
+  },
+  bgImg: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    width: '100%',
+    height: '100%',
+  },
+  heroText: {
+    backgroundColor: colors.white,
+    paddingTop: 10,
+    paddingBottom: 20,
+  },
+  btns: {
+    paddingBottom: 40,
+  },
+  title: {
+    textAlign: 'center',
+    color: colors.blue3,
+    fontSize: 68,
+    fontWeight: 'bold',
+  },
+  btn: {
+    borderRadius: 4,
+    paddingVertical: 16,
+    marginBottom: 20,
+    marginHorizontal: 20,
+    backgroundColor: colors.blue3,
+  },
+  btnLabel: {
+    textAlign: 'center',
+    fontSize: 21,
+    color: colors.white,
+  },
+})
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
new file mode 100644
index 000000000..05d0355d9
--- /dev/null
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -0,0 +1,102 @@
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {Text} from 'view/com/util/text/Text'
+import {TextLink} from '../util/Link'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
+import {s, colors} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {CenteredView} from '../util/Views'
+
+export const SplashScreen = ({
+  onPressSignin,
+  onPressCreateAccount,
+}: {
+  onPressSignin: () => void
+  onPressCreateAccount: () => void
+}) => {
+  const pal = usePalette('default')
+  return (
+    <CenteredView style={styles.container}>
+      <View testID="noSessionView" style={styles.containerInner}>
+        <ErrorBoundary>
+          <Text style={styles.title}>Bluesky</Text>
+          <Text style={styles.subtitle}>See what's next</Text>
+          <View testID="signinOrCreateAccount" style={styles.btns}>
+            <TouchableOpacity
+              testID="createAccountButton"
+              style={[styles.btn, {backgroundColor: colors.blue3}]}
+              onPress={onPressCreateAccount}>
+              <Text style={[s.white, styles.btnLabel]}>
+                Create a new account
+              </Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              testID="signInButton"
+              style={[styles.btn, pal.btn]}
+              onPress={onPressSignin}>
+              <Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
+            </TouchableOpacity>
+          </View>
+          <Text
+            type="xl"
+            style={[styles.notice, pal.textLight]}
+            lineHeight={1.3}>
+            Bluesky will launch soon.{' '}
+            <TextLink
+              type="xl"
+              text="Join the waitlist"
+              href="#"
+              style={pal.link}
+            />{' '}
+            to try the beta before it's publicly available.
+          </Text>
+        </ErrorBoundary>
+      </View>
+    </CenteredView>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    height: '100%',
+    backgroundColor: colors.gray1,
+  },
+  containerInner: {
+    backgroundColor: colors.white,
+    paddingVertical: 40,
+    paddingBottom: 50,
+    paddingHorizontal: 20,
+  },
+  title: {
+    textAlign: 'center',
+    color: colors.blue3,
+    fontSize: 68,
+    fontWeight: 'bold',
+    paddingBottom: 10,
+  },
+  subtitle: {
+    textAlign: 'center',
+    color: colors.gray5,
+    fontSize: 52,
+    fontWeight: 'bold',
+    paddingBottom: 30,
+  },
+  btns: {
+    flexDirection: 'row',
+    paddingBottom: 40,
+  },
+  btn: {
+    flex: 1,
+    borderRadius: 30,
+    paddingVertical: 12,
+    marginHorizontal: 10,
+  },
+  btnLabel: {
+    textAlign: 'center',
+    fontSize: 18,
+  },
+  notice: {
+    paddingHorizontal: 40,
+    textAlign: 'center',
+  },
+})
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
new file mode 100644
index 000000000..11b67f383
--- /dev/null
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -0,0 +1,47 @@
+import React from 'react'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {useStores} from 'state/index'
+import {LoggedOut} from './LoggedOut'
+import {Text} from '../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+
+export const withAuthRequired = <P extends object>(
+  Component: React.ComponentType<P>,
+): React.FC<P> =>
+  observer((props: P) => {
+    const store = useStores()
+    if (store.session.isResumingSession) {
+      return <Loading />
+    }
+    if (!store.session.hasSession) {
+      return <LoggedOut />
+    }
+    return <Component {...props} />
+  })
+
+function Loading() {
+  const pal = usePalette('default')
+  return (
+    <View style={[styles.loading, pal.view]}>
+      <ActivityIndicator size="large" />
+      <Text type="2xl" style={[styles.loadingText, pal.textLight]}>
+        Firing up the grill...
+      </Text>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  loading: {
+    height: '100%',
+    alignContent: 'center',
+    justifyContent: 'center',
+    paddingBottom: 100,
+  },
+  loadingText: {
+    paddingVertical: 20,
+    paddingHorizontal: 20,
+    textAlign: 'center',
+  },
+})
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 505b1fcfe..adc73315c 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -4,6 +4,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/posts/Feed'
 import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
@@ -18,95 +19,97 @@ import {ComposeIcon2} from 'lib/icons'
 const HEADER_HEIGHT = 42
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
-export const HomeScreen = observer(function Home(_opts: Props) {
-  const store = useStores()
-  const onMainScroll = useOnMainScroll(store)
-  const {screen, track} = useAnalytics()
-  const scrollElRef = React.useRef<FlatList>(null)
-  const {appState} = useAppState({
-    onForeground: () => doPoll(true),
-  })
-  const isFocused = useIsFocused()
+export const HomeScreen = withAuthRequired(
+  observer(function Home(_opts: Props) {
+    const store = useStores()
+    const onMainScroll = useOnMainScroll(store)
+    const {screen, track} = useAnalytics()
+    const scrollElRef = React.useRef<FlatList>(null)
+    const {appState} = useAppState({
+      onForeground: () => doPoll(true),
+    })
+    const isFocused = useIsFocused()
 
-  const doPoll = React.useCallback(
-    (knownActive = false) => {
-      if ((!knownActive && appState !== 'active') || !isFocused) {
-        return
-      }
-      if (store.me.mainFeed.isLoading) {
-        return
-      }
-      store.log.debug('HomeScreen: Polling for new posts')
-      store.me.mainFeed.checkForLatest()
-    },
-    [appState, isFocused, store],
-  )
+    const doPoll = React.useCallback(
+      (knownActive = false) => {
+        if ((!knownActive && appState !== 'active') || !isFocused) {
+          return
+        }
+        if (store.me.mainFeed.isLoading) {
+          return
+        }
+        store.log.debug('HomeScreen: Polling for new posts')
+        store.me.mainFeed.checkForLatest()
+      },
+      [appState, isFocused, store],
+    )
 
-  const scrollToTop = React.useCallback(() => {
-    // NOTE: the feed is offset by the height of the collapsing header,
-    //       so we scroll to the negative of that height -prf
-    scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
-  }, [scrollElRef])
+    const scrollToTop = React.useCallback(() => {
+      // NOTE: the feed is offset by the height of the collapsing header,
+      //       so we scroll to the negative of that height -prf
+      scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
+    }, [scrollElRef])
 
-  useFocusEffect(
-    React.useCallback(() => {
-      const softResetSub = store.onScreenSoftReset(scrollToTop)
-      const feedCleanup = store.me.mainFeed.registerListeners()
-      const pollInterval = setInterval(doPoll, 15e3)
+    useFocusEffect(
+      React.useCallback(() => {
+        const softResetSub = store.onScreenSoftReset(scrollToTop)
+        const feedCleanup = store.me.mainFeed.registerListeners()
+        const pollInterval = setInterval(doPoll, 15e3)
 
-      screen('Feed')
-      store.log.debug('HomeScreen: Updating feed')
-      if (store.me.mainFeed.hasContent) {
-        store.me.mainFeed.update()
-      }
+        screen('Feed')
+        store.log.debug('HomeScreen: Updating feed')
+        if (store.me.mainFeed.hasContent) {
+          store.me.mainFeed.update()
+        }
 
-      return () => {
-        clearInterval(pollInterval)
-        softResetSub.remove()
-        feedCleanup()
-      }
-    }, [store, doPoll, scrollToTop, screen]),
-  )
+        return () => {
+          clearInterval(pollInterval)
+          softResetSub.remove()
+          feedCleanup()
+        }
+      }, [store, doPoll, scrollToTop, screen]),
+    )
 
-  const onPressCompose = React.useCallback(() => {
-    track('HomeScreen:PressCompose')
-    store.shell.openComposer({})
-  }, [store, track])
+    const onPressCompose = React.useCallback(() => {
+      track('HomeScreen:PressCompose')
+      store.shell.openComposer({})
+    }, [store, track])
 
-  const onPressTryAgain = React.useCallback(() => {
-    store.me.mainFeed.refresh()
-  }, [store])
+    const onPressTryAgain = React.useCallback(() => {
+      store.me.mainFeed.refresh()
+    }, [store])
 
-  const onPressLoadLatest = React.useCallback(() => {
-    store.me.mainFeed.refresh()
-    scrollToTop()
-  }, [store, scrollToTop])
+    const onPressLoadLatest = React.useCallback(() => {
+      store.me.mainFeed.refresh()
+      scrollToTop()
+    }, [store, scrollToTop])
 
-  return (
-    <View style={s.hContentRegion}>
-      {store.shell.isOnboarding && <WelcomeBanner />}
-      <Feed
-        testID="homeFeed"
-        key="default"
-        feed={store.me.mainFeed}
-        scrollElRef={scrollElRef}
-        style={s.hContentRegion}
-        showPostFollowBtn
-        onPressTryAgain={onPressTryAgain}
-        onScroll={onMainScroll}
-        headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT}
-      />
-      {!store.shell.isOnboarding && (
-        <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
-      )}
-      {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
-        <LoadLatestBtn onPress={onPressLoadLatest} />
-      )}
-      <FAB
-        testID="composeFAB"
-        onPress={onPressCompose}
-        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-      />
-    </View>
-  )
-})
+    return (
+      <View style={s.hContentRegion}>
+        {store.shell.isOnboarding && <WelcomeBanner />}
+        <Feed
+          testID="homeFeed"
+          key="default"
+          feed={store.me.mainFeed}
+          scrollElRef={scrollElRef}
+          style={s.hContentRegion}
+          showPostFollowBtn
+          onPressTryAgain={onPressTryAgain}
+          onScroll={onMainScroll}
+          headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT}
+        />
+        {!store.shell.isOnboarding && (
+          <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
+        )}
+        {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
+          <LoadLatestBtn onPress={onPressLoadLatest} />
+        )}
+        <FAB
+          testID="composeFAB"
+          onPress={onPressCompose}
+          icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
+        />
+      </View>
+    )
+  }),
+)
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
deleted file mode 100644
index 8ff9fc9c2..000000000
--- a/src/view/screens/Login.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, {useEffect, useState} from 'react'
-import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
-import Image, {Source as ImageSource} from 'view/com/util/images/Image'
-import {observer} from 'mobx-react-lite'
-import {Signin} from '../com/login/Signin'
-import {CreateAccount} from '../com/login/CreateAccount'
-import {Text} from '../com/util/text/Text'
-import {ErrorBoundary} from '../com/util/ErrorBoundary'
-import {colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
-import {CLOUD_SPLASH} from 'lib/assets'
-import {useAnalytics} from 'lib/analytics'
-
-enum ScreenState {
-  S_SigninOrCreateAccount,
-  S_Signin,
-  S_CreateAccount,
-}
-
-const SigninOrCreateAccount = ({
-  onPressSignin,
-  onPressCreateAccount,
-}: {
-  onPressSignin: () => void
-  onPressCreateAccount: () => void
-}) => {
-  const {screen} = useAnalytics()
-
-  useEffect(() => {
-    screen('Login')
-  }, [screen])
-
-  const pal = usePalette('default')
-  return (
-    <>
-      <View style={styles.hero}>
-        <View style={styles.heroText}>
-          <Text style={styles.title}>Bluesky</Text>
-        </View>
-      </View>
-      <View testID="signinOrCreateAccount" style={styles.btns}>
-        <TouchableOpacity
-          testID="createAccountButton"
-          style={[pal.view, styles.btn]}
-          onPress={onPressCreateAccount}>
-          <Text style={[pal.link, styles.btnLabel]}>Create a new account</Text>
-        </TouchableOpacity>
-        <TouchableOpacity
-          testID="signInButton"
-          style={[pal.view, styles.btn]}
-          onPress={onPressSignin}>
-          <Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
-        </TouchableOpacity>
-      </View>
-    </>
-  )
-}
-
-export const Login = observer(() => {
-  const pal = usePalette('default')
-  const store = useStores()
-  const [screenState, setScreenState] = useState<ScreenState>(
-    ScreenState.S_SigninOrCreateAccount,
-  )
-
-  if (
-    store.session.isResumingSession ||
-    screenState === ScreenState.S_SigninOrCreateAccount
-  ) {
-    return (
-      <View style={styles.container}>
-        <Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
-        <SafeAreaView testID="noSessionView" style={styles.container}>
-          <ErrorBoundary>
-            {!store.session.isResumingSession && (
-              <SigninOrCreateAccount
-                onPressSignin={() => setScreenState(ScreenState.S_Signin)}
-                onPressCreateAccount={() =>
-                  setScreenState(ScreenState.S_CreateAccount)
-                }
-              />
-            )}
-          </ErrorBoundary>
-        </SafeAreaView>
-      </View>
-    )
-  }
-
-  return (
-    <View style={[styles.container, pal.view]}>
-      <SafeAreaView testID="noSessionView" style={styles.container}>
-        <ErrorBoundary>
-          {screenState === ScreenState.S_Signin ? (
-            <Signin
-              onPressBack={() =>
-                setScreenState(ScreenState.S_SigninOrCreateAccount)
-              }
-            />
-          ) : undefined}
-          {screenState === ScreenState.S_CreateAccount ? (
-            <CreateAccount
-              onPressBack={() =>
-                setScreenState(ScreenState.S_SigninOrCreateAccount)
-              }
-            />
-          ) : undefined}
-        </ErrorBoundary>
-      </SafeAreaView>
-    </View>
-  )
-})
-
-const styles = StyleSheet.create({
-  container: {
-    height: '100%',
-  },
-  outer: {
-    flex: 1,
-  },
-  hero: {
-    flex: 2,
-    justifyContent: 'center',
-  },
-  bgImg: {
-    position: 'absolute',
-    top: 0,
-    left: 0,
-    width: '100%',
-    height: '100%',
-  },
-  heroText: {
-    backgroundColor: colors.white,
-    paddingTop: 10,
-    paddingBottom: 20,
-  },
-  btns: {
-    paddingBottom: 40,
-  },
-  title: {
-    textAlign: 'center',
-    color: colors.blue3,
-    fontSize: 68,
-    fontWeight: 'bold',
-  },
-  subtitle: {
-    textAlign: 'center',
-    color: colors.blue3,
-    fontSize: 18,
-  },
-  btn: {
-    borderRadius: 4,
-    paddingVertical: 16,
-    marginBottom: 20,
-    marginHorizontal: 20,
-    backgroundColor: colors.blue3,
-  },
-  btnLabel: {
-    textAlign: 'center',
-    fontSize: 21,
-    // fontWeight: '500',
-    color: colors.white,
-  },
-})
diff --git a/src/view/screens/Login.web.tsx b/src/view/screens/Login.web.tsx
deleted file mode 100644
index 1980d8dc5..000000000
--- a/src/view/screens/Login.web.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React, {useState} from 'react'
-import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import {CenteredView} from '../com/util/Views'
-import {Signin} from '../com/login/Signin'
-import {CreateAccount} from '../com/login/CreateAccount'
-import {Text} from '../com/util/text/Text'
-import {ErrorBoundary} from '../com/util/ErrorBoundary'
-import {colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-
-enum ScreenState {
-  S_SigninOrCreateAccount,
-  S_Signin,
-  S_CreateAccount,
-}
-
-const SigninOrCreateAccount = ({
-  onPressSignin,
-  onPressCreateAccount,
-}: {
-  onPressSignin: () => void
-  onPressCreateAccount: () => void
-}) => {
-  const pal = usePalette('default')
-  return (
-    <>
-      <View style={styles.hero}>
-        <View style={styles.heroText}>
-          <Text style={styles.title}>Bluesky</Text>
-        </View>
-      </View>
-      <View testID="signinOrCreateAccount" style={styles.btns}>
-        <TouchableOpacity
-          testID="createAccountButton"
-          style={[pal.view, styles.btn]}
-          onPress={onPressCreateAccount}>
-          <Text style={[pal.link, styles.btnLabel]}>New account</Text>
-        </TouchableOpacity>
-        <TouchableOpacity
-          testID="signInButton"
-          style={[pal.view, styles.btn]}
-          onPress={onPressSignin}>
-          <Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
-        </TouchableOpacity>
-      </View>
-    </>
-  )
-}
-
-export const Login = observer(() => {
-  const pal = usePalette('default')
-  const [screenState, setScreenState] = useState<ScreenState>(
-    ScreenState.S_SigninOrCreateAccount,
-  )
-
-  if (screenState === ScreenState.S_SigninOrCreateAccount) {
-    return (
-      <CenteredView style={[styles.container, styles.vertCenter]}>
-        <ErrorBoundary>
-          <SigninOrCreateAccount
-            onPressSignin={() => setScreenState(ScreenState.S_Signin)}
-            onPressCreateAccount={() =>
-              setScreenState(ScreenState.S_CreateAccount)
-            }
-          />
-        </ErrorBoundary>
-      </CenteredView>
-    )
-  }
-
-  return (
-    <CenteredView
-      style={[
-        styles.container,
-        styles.containerBorder,
-        pal.view,
-        pal.borderDark,
-      ]}>
-      <SafeAreaView testID="noSessionView" style={styles.container}>
-        <ErrorBoundary>
-          {screenState === ScreenState.S_Signin ? (
-            <Signin
-              onPressBack={() =>
-                setScreenState(ScreenState.S_SigninOrCreateAccount)
-              }
-            />
-          ) : undefined}
-          {screenState === ScreenState.S_CreateAccount ? (
-            <CreateAccount
-              onPressBack={() =>
-                setScreenState(ScreenState.S_SigninOrCreateAccount)
-              }
-            />
-          ) : undefined}
-        </ErrorBoundary>
-      </SafeAreaView>
-    </CenteredView>
-  )
-})
-
-const styles = StyleSheet.create({
-  container: {
-    height: '100%',
-  },
-  containerBorder: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  vertCenter: {
-    justifyContent: 'center',
-  },
-  bgImg: {
-    position: 'absolute',
-    top: 0,
-    left: 0,
-    width: '100%',
-    height: '100%',
-  },
-  hero: {},
-  heroText: {
-    backgroundColor: colors.white,
-    paddingTop: 10,
-    paddingBottom: 20,
-  },
-  btns: {
-    flexDirection: 'row',
-    paddingTop: 40,
-  },
-  title: {
-    textAlign: 'center',
-    color: colors.blue3,
-    fontSize: 68,
-    fontWeight: 'bold',
-  },
-  subtitle: {
-    textAlign: 'center',
-    color: colors.blue3,
-    fontSize: 18,
-  },
-  btn: {
-    flex: 1,
-    borderRadius: 4,
-    paddingVertical: 16,
-    marginBottom: 20,
-    marginHorizontal: 20,
-    borderWidth: 1,
-    borderColor: colors.blue3,
-  },
-  btnLabel: {
-    textAlign: 'center',
-    fontSize: 21,
-    fontWeight: '500',
-    color: colors.blue3,
-  },
-})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 492177d1f..b704f9c45 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,11 +1,13 @@
 import React, {useEffect} from 'react'
 import {FlatList, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
+import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
 import {
   NativeStackScreenProps,
   NotificationsTabNavigatorParams,
 } from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/notifications/Feed'
 import {useStores} from 'state/index'
@@ -19,77 +21,79 @@ type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
   'Notifications'
 >
-export const NotificationsScreen = ({}: Props) => {
-  const store = useStores()
-  const onMainScroll = useOnMainScroll(store)
-  const scrollElRef = React.useRef<FlatList>(null)
-  const {screen} = useAnalytics()
-  const {appState} = useAppState({
-    onForeground: () => doPoll(true),
-  })
+export const NotificationsScreen = withAuthRequired(
+  observer(({}: Props) => {
+    const store = useStores()
+    const onMainScroll = useOnMainScroll(store)
+    const scrollElRef = React.useRef<FlatList>(null)
+    const {screen} = useAnalytics()
+    const {appState} = useAppState({
+      onForeground: () => doPoll(true),
+    })
 
-  // event handlers
-  // =
-  const onPressTryAgain = () => {
-    store.me.notifications.refresh()
-  }
-  const scrollToTop = React.useCallback(() => {
-    scrollElRef.current?.scrollToOffset({offset: 0})
-  }, [scrollElRef])
+    // event handlers
+    // =
+    const onPressTryAgain = () => {
+      store.me.notifications.refresh()
+    }
+    const scrollToTop = React.useCallback(() => {
+      scrollElRef.current?.scrollToOffset({offset: 0})
+    }, [scrollElRef])
 
-  // periodic polling
-  // =
-  const doPoll = React.useCallback(
-    async (isForegrounding = false) => {
-      if (isForegrounding) {
-        // app is foregrounding, refresh optimistically
-        store.log.debug('NotificationsScreen: Refreshing on app foreground')
-        await Promise.all([
-          store.me.notifications.loadUnreadCount(),
-          store.me.notifications.refresh(),
-        ])
-      } else if (appState === 'active') {
-        // periodic poll, refresh if there are new notifs
-        store.log.debug('NotificationsScreen: Polling for new notifications')
-        const didChange = await store.me.notifications.loadUnreadCount()
-        if (didChange) {
-          store.log.debug('NotificationsScreen: Loading new notifications')
-          await store.me.notifications.loadLatest()
+    // periodic polling
+    // =
+    const doPoll = React.useCallback(
+      async (isForegrounding = false) => {
+        if (isForegrounding) {
+          // app is foregrounding, refresh optimistically
+          store.log.debug('NotificationsScreen: Refreshing on app foreground')
+          await Promise.all([
+            store.me.notifications.loadUnreadCount(),
+            store.me.notifications.refresh(),
+          ])
+        } else if (appState === 'active') {
+          // periodic poll, refresh if there are new notifs
+          store.log.debug('NotificationsScreen: Polling for new notifications')
+          const didChange = await store.me.notifications.loadUnreadCount()
+          if (didChange) {
+            store.log.debug('NotificationsScreen: Loading new notifications')
+            await store.me.notifications.loadLatest()
+          }
         }
-      }
-    },
-    [appState, store],
-  )
-  useEffect(() => {
-    const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL)
-    return () => clearInterval(pollInterval)
-  }, [doPoll])
+      },
+      [appState, store],
+    )
+    useEffect(() => {
+      const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL)
+      return () => clearInterval(pollInterval)
+    }, [doPoll])
 
-  // on-visible setup
-  // =
-  useFocusEffect(
-    React.useCallback(() => {
-      store.log.debug('NotificationsScreen: Updating feed')
-      const softResetSub = store.onScreenSoftReset(scrollToTop)
-      store.me.notifications.update()
-      screen('Notifications')
+    // on-visible setup
+    // =
+    useFocusEffect(
+      React.useCallback(() => {
+        store.log.debug('NotificationsScreen: Updating feed')
+        const softResetSub = store.onScreenSoftReset(scrollToTop)
+        store.me.notifications.update()
+        screen('Notifications')
 
-      return () => {
-        softResetSub.remove()
-        store.me.notifications.markAllRead()
-      }
-    }, [store, screen, scrollToTop]),
-  )
+        return () => {
+          softResetSub.remove()
+          store.me.notifications.markAllRead()
+        }
+      }, [store, screen, scrollToTop]),
+    )
 
-  return (
-    <View style={s.hContentRegion}>
-      <ViewHeader title="Notifications" canGoBack={false} />
-      <Feed
-        view={store.me.notifications}
-        onPressTryAgain={onPressTryAgain}
-        onScroll={onMainScroll}
-        scrollElRef={scrollElRef}
-      />
-    </View>
-  )
-}
+    return (
+      <View style={s.hContentRegion}>
+        <ViewHeader title="Notifications" canGoBack={false} />
+        <Feed
+          view={store.me.notifications}
+          onPressTryAgain={onPressTryAgain}
+          onScroll={onMainScroll}
+          scrollElRef={scrollElRef}
+        />
+      </View>
+    )
+  }),
+)
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index 1a63445e5..19f0af18b 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
@@ -8,7 +9,7 @@ import {useStores} from 'state/index'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
-export const PostRepostedByScreen = ({route}: Props) => {
+export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
   const store = useStores()
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@@ -25,4 +26,4 @@ export const PostRepostedByScreen = ({route}: Props) => {
       <PostRepostedByComponent uri={uri} />
     </View>
   )
-}
+})
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 0e9feae0b..ad54126b6 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
 import {ComposePrompt} from 'view/com/composer/Prompt'
@@ -16,7 +17,7 @@ import {isDesktopWeb} from 'platform/detection'
 const SHELL_FOOTER_HEIGHT = 44
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
-export const PostThreadScreen = ({route}: Props) => {
+export const PostThreadScreen = withAuthRequired(({route}: Props) => {
   const store = useStores()
   const safeAreaInsets = useSafeAreaInsets()
   const {name, rkey} = route.params
@@ -84,7 +85,7 @@ export const PostThreadScreen = ({route}: Props) => {
       )}
     </View>
   )
-}
+})
 
 const styles = StyleSheet.create({
   prompt: {
diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx
index b1690721b..35b55f3c4 100644
--- a/src/view/screens/PostUpvotedBy.tsx
+++ b/src/view/screens/PostUpvotedBy.tsx
@@ -2,13 +2,14 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
 import {useStores} from 'state/index'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostUpvotedBy'>
-export const PostUpvotedByScreen = ({route}: Props) => {
+export const PostUpvotedByScreen = withAuthRequired(({route}: Props) => {
   const store = useStores()
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@@ -25,4 +26,4 @@ export const PostUpvotedByScreen = ({route}: Props) => {
       <PostLikedByComponent uri={uri} direction="up" />
     </View>
   )
-}
+})
diff --git a/src/view/screens/PrivacyPolicy.tsx b/src/view/screens/PrivacyPolicy.tsx
index d5476ab52..ec39ac2d8 100644
--- a/src/view/screens/PrivacyPolicy.tsx
+++ b/src/view/screens/PrivacyPolicy.tsx
@@ -26,7 +26,7 @@ export const PrivacyPolicyScreen = (_props: Props) => {
       <ViewHeader title="Privacy Policy" />
       <ScrollView style={[s.hContentRegion, pal.view]}>
         <View style={[s.p20]}>
-          <Text type="title-xl" style={[pal.text, s.pb20]}>
+          <Text type="title-xl" style={[pal.text, s.bold, s.pb20]}>
             Privacy Policy
           </Text>
           <PrivacyPolicyHtml />
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index e0d0a5884..b5073f28d 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -3,6 +3,7 @@ import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewSelector} from '../com/util/ViewSelector'
 import {CenteredView} from '../com/util/Views'
 import {ProfileUiModel, Sections} from 'state/models/profile-ui'
@@ -25,178 +26,182 @@ const END_ITEM = {_reactKey: '__end__'}
 const EMPTY_ITEM = {_reactKey: '__empty__'}
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
-export const ProfileScreen = observer(({route}: Props) => {
-  const store = useStores()
-  const {screen, track} = useAnalytics()
+export const ProfileScreen = withAuthRequired(
+  observer(({route}: Props) => {
+    const store = useStores()
+    const {screen, track} = useAnalytics()
 
-  useEffect(() => {
-    screen('Profile')
-  }, [screen])
+    useEffect(() => {
+      screen('Profile')
+    }, [screen])
 
-  const onMainScroll = useOnMainScroll(store)
-  const [hasSetup, setHasSetup] = useState<boolean>(false)
-  const uiState = React.useMemo(
-    () => new ProfileUiModel(store, {user: route.params.name}),
-    [route.params.name, store],
-  )
+    const onMainScroll = useOnMainScroll(store)
+    const [hasSetup, setHasSetup] = useState<boolean>(false)
+    const uiState = React.useMemo(
+      () => new ProfileUiModel(store, {user: route.params.name}),
+      [route.params.name, store],
+    )
 
-  useFocusEffect(
-    React.useCallback(() => {
-      let aborted = false
-      const feedCleanup = uiState.feed.registerListeners()
-      if (hasSetup) {
-        uiState.update()
-      } else {
-        uiState.setup().then(() => {
-          if (aborted) {
-            return
-          }
-          setHasSetup(true)
-        })
-      }
-      return () => {
-        aborted = true
-        feedCleanup()
-      }
-    }, [hasSetup, uiState]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        let aborted = false
+        const feedCleanup = uiState.feed.registerListeners()
+        if (hasSetup) {
+          uiState.update()
+        } else {
+          uiState.setup().then(() => {
+            if (aborted) {
+              return
+            }
+            setHasSetup(true)
+          })
+        }
+        return () => {
+          aborted = true
+          feedCleanup()
+        }
+      }, [hasSetup, uiState]),
+    )
 
-  // events
-  // =
+    // events
+    // =
 
-  const onPressCompose = React.useCallback(() => {
-    track('ProfileScreen:PressCompose')
-    store.shell.openComposer({})
-  }, [store, track])
-  const onSelectView = (index: number) => {
-    uiState.setSelectedViewIndex(index)
-  }
-  const onRefresh = () => {
-    uiState
-      .refresh()
-      .catch((err: any) =>
-        store.log.error('Failed to refresh user profile', err),
-      )
-  }
-  const onEndReached = () => {
-    uiState
-      .loadMore()
-      .catch((err: any) =>
-        store.log.error('Failed to load more entries in user profile', err),
-      )
-  }
-  const onPressTryAgain = () => {
-    uiState.setup()
-  }
+    const onPressCompose = React.useCallback(() => {
+      track('ProfileScreen:PressCompose')
+      store.shell.openComposer({})
+    }, [store, track])
+    const onSelectView = (index: number) => {
+      uiState.setSelectedViewIndex(index)
+    }
+    const onRefresh = () => {
+      uiState
+        .refresh()
+        .catch((err: any) =>
+          store.log.error('Failed to refresh user profile', err),
+        )
+    }
+    const onEndReached = () => {
+      uiState
+        .loadMore()
+        .catch((err: any) =>
+          store.log.error('Failed to load more entries in user profile', err),
+        )
+    }
+    const onPressTryAgain = () => {
+      uiState.setup()
+    }
 
-  // rendering
-  // =
+    // rendering
+    // =
 
-  const renderHeader = () => {
-    if (!uiState) {
-      return <View />
+    const renderHeader = () => {
+      if (!uiState) {
+        return <View />
+      }
+      return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
     }
-    return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
-  }
-  let renderItem
-  let Footer
-  let items: any[] = []
-  if (uiState) {
-    if (uiState.isInitialLoading) {
-      items = items.concat([LOADING_ITEM])
-      renderItem = () => <PostFeedLoadingPlaceholder />
-    } else if (uiState.currentView.hasError) {
-      items = items.concat([
-        {
-          _reactKey: '__error__',
-          error: uiState.currentView.error,
-        },
-      ])
-      renderItem = (item: any) => (
-        <View style={s.p5}>
-          <ErrorMessage
-            message={item.error}
-            onPressTryAgain={onPressTryAgain}
-          />
-        </View>
-      )
-    } else {
-      if (
-        uiState.selectedView === Sections.Posts ||
-        uiState.selectedView === Sections.PostsWithReplies
-      ) {
-        if (uiState.feed.hasContent) {
-          if (uiState.selectedView === Sections.Posts) {
-            items = uiState.feed.nonReplyFeed
-          } else {
-            items = uiState.feed.feed.slice()
-          }
-          if (!uiState.feed.hasMore) {
-            items = items.concat([END_ITEM])
-          } else if (uiState.feed.isLoading) {
-            Footer = LoadingMoreFooter
-          }
-          renderItem = (item: any) => {
-            if (item === END_ITEM) {
-              return <Text style={styles.endItem}>- end of feed -</Text>
+    let renderItem
+    let Footer
+    let items: any[] = []
+    if (uiState) {
+      if (uiState.isInitialLoading) {
+        items = items.concat([LOADING_ITEM])
+        renderItem = () => <PostFeedLoadingPlaceholder />
+      } else if (uiState.currentView.hasError) {
+        items = items.concat([
+          {
+            _reactKey: '__error__',
+            error: uiState.currentView.error,
+          },
+        ])
+        renderItem = (item: any) => (
+          <View style={s.p5}>
+            <ErrorMessage
+              message={item.error}
+              onPressTryAgain={onPressTryAgain}
+            />
+          </View>
+        )
+      } else {
+        if (
+          uiState.selectedView === Sections.Posts ||
+          uiState.selectedView === Sections.PostsWithReplies
+        ) {
+          if (uiState.feed.hasContent) {
+            if (uiState.selectedView === Sections.Posts) {
+              items = uiState.feed.nonReplyFeed
+            } else {
+              items = uiState.feed.feed.slice()
+            }
+            if (!uiState.feed.hasMore) {
+              items = items.concat([END_ITEM])
+            } else if (uiState.feed.isLoading) {
+              Footer = LoadingMoreFooter
             }
-            return <FeedItem item={item} ignoreMuteFor={uiState.profile.did} />
+            renderItem = (item: any) => {
+              if (item === END_ITEM) {
+                return <Text style={styles.endItem}>- end of feed -</Text>
+              }
+              return (
+                <FeedItem item={item} ignoreMuteFor={uiState.profile.did} />
+              )
+            }
+          } else if (uiState.feed.isEmpty) {
+            items = items.concat([EMPTY_ITEM])
+            renderItem = () => (
+              <EmptyState
+                icon={['far', 'message']}
+                message="No posts yet!"
+                style={styles.emptyState}
+              />
+            )
           }
-        } else if (uiState.feed.isEmpty) {
+        } else {
           items = items.concat([EMPTY_ITEM])
-          renderItem = () => (
-            <EmptyState
-              icon={['far', 'message']}
-              message="No posts yet!"
-              style={styles.emptyState}
-            />
-          )
+          renderItem = () => <Text>TODO</Text>
         }
-      } else {
-        items = items.concat([EMPTY_ITEM])
-        renderItem = () => <Text>TODO</Text>
       }
     }
-  }
-  if (!renderItem) {
-    renderItem = () => <View />
-  }
+    if (!renderItem) {
+      renderItem = () => <View />
+    }
 
-  return (
-    <View testID="profileView" style={styles.container}>
-      {uiState.profile.hasError ? (
-        <ErrorScreen
-          testID="profileErrorScreen"
-          title="Failed to load profile"
-          message={`There was an issue when attempting to load ${route.params.name}`}
-          details={uiState.profile.error}
-          onPressTryAgain={onPressTryAgain}
-        />
-      ) : uiState.profile.hasLoaded ? (
-        <ViewSelector
-          swipeEnabled
-          sections={uiState.selectorItems}
-          items={items}
-          renderHeader={renderHeader}
-          renderItem={renderItem}
-          ListFooterComponent={Footer}
-          refreshing={uiState.isRefreshing || false}
-          onSelectView={onSelectView}
-          onScroll={onMainScroll}
-          onRefresh={onRefresh}
-          onEndReached={onEndReached}
+    return (
+      <View testID="profileView" style={styles.container}>
+        {uiState.profile.hasError ? (
+          <ErrorScreen
+            testID="profileErrorScreen"
+            title="Failed to load profile"
+            message={`There was an issue when attempting to load ${route.params.name}`}
+            details={uiState.profile.error}
+            onPressTryAgain={onPressTryAgain}
+          />
+        ) : uiState.profile.hasLoaded ? (
+          <ViewSelector
+            swipeEnabled
+            sections={uiState.selectorItems}
+            items={items}
+            renderHeader={renderHeader}
+            renderItem={renderItem}
+            ListFooterComponent={Footer}
+            refreshing={uiState.isRefreshing || false}
+            onSelectView={onSelectView}
+            onScroll={onMainScroll}
+            onRefresh={onRefresh}
+            onEndReached={onEndReached}
+          />
+        ) : (
+          <CenteredView>{renderHeader()}</CenteredView>
+        )}
+        <FAB
+          testID="composeFAB"
+          onPress={onPressCompose}
+          icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
         />
-      ) : (
-        <CenteredView>{renderHeader()}</CenteredView>
-      )}
-      <FAB
-        testID="composeFAB"
-        onPress={onPressCompose}
-        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-      />
-    </View>
-  )
-})
+      </View>
+    )
+  }),
+)
 
 function LoadingMoreFooter() {
   return (
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index b248cdc3a..e2f95fbe4 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -2,12 +2,13 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
 import {useStores} from 'state/index'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
-export const ProfileFollowersScreen = ({route}: Props) => {
+export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
   const store = useStores()
   const {name} = route.params
 
@@ -23,4 +24,4 @@ export const ProfileFollowersScreen = ({route}: Props) => {
       <ProfileFollowersComponent name={name} />
     </View>
   )
-}
+})
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 7edf8edba..f70944f55 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -2,12 +2,13 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
 import {useStores} from 'state/index'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
-export const ProfileFollowsScreen = ({route}: Props) => {
+export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
   const store = useStores()
   const {name} = route.params
 
@@ -23,4 +24,4 @@ export const ProfileFollowsScreen = ({route}: Props) => {
       <ProfileFollowsComponent name={name} />
     </View>
   )
-}
+})
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index a50d5c6a7..19535a164 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -12,6 +12,7 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ScrollView} from '../com/util/Views'
 import {
   NativeStackScreenProps,
@@ -36,159 +37,161 @@ const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
 const FIVE_MIN = 5 * 60 * 1e3
 
 type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
-export const SearchScreen = observer<Props>(({}: Props) => {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const store = useStores()
-  const {track} = useAnalytics()
-  const scrollElRef = React.useRef<ScrollView>(null)
-  const onMainScroll = useOnMainScroll(store)
-  const textInput = React.useRef<TextInput>(null)
-  const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
-  const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
-  const [query, setQuery] = React.useState<string>('')
-  const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
-    () => new UserAutocompleteViewModel(store),
-    [store],
-  )
+export const SearchScreen = withAuthRequired(
+  observer<Props>(({}: Props) => {
+    const pal = usePalette('default')
+    const theme = useTheme()
+    const store = useStores()
+    const {track} = useAnalytics()
+    const scrollElRef = React.useRef<ScrollView>(null)
+    const onMainScroll = useOnMainScroll(store)
+    const textInput = React.useRef<TextInput>(null)
+    const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
+    const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
+    const [query, setQuery] = React.useState<string>('')
+    const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
+      () => new UserAutocompleteViewModel(store),
+      [store],
+    )
 
-  const onSoftReset = () => {
-    scrollElRef.current?.scrollTo({x: 0, y: 0})
-  }
+    const onSoftReset = () => {
+      scrollElRef.current?.scrollTo({x: 0, y: 0})
+    }
 
-  useFocusEffect(
-    React.useCallback(() => {
-      const softResetSub = store.onScreenSoftReset(onSoftReset)
-      const cleanup = () => {
-        softResetSub.remove()
-      }
+    useFocusEffect(
+      React.useCallback(() => {
+        const softResetSub = store.onScreenSoftReset(onSoftReset)
+        const cleanup = () => {
+          softResetSub.remove()
+        }
 
-      const now = Date.now()
-      if (now - lastRenderTime > FIVE_MIN) {
-        setRenderTime(Date.now()) // trigger reload of suggestions
-      }
-      store.shell.setMinimalShellMode(false)
-      autocompleteView.setup()
+        const now = Date.now()
+        if (now - lastRenderTime > FIVE_MIN) {
+          setRenderTime(Date.now()) // trigger reload of suggestions
+        }
+        store.shell.setMinimalShellMode(false)
+        autocompleteView.setup()
 
-      return cleanup
-    }, [store, autocompleteView, lastRenderTime, setRenderTime]),
-  )
+        return cleanup
+      }, [store, autocompleteView, lastRenderTime, setRenderTime]),
+    )
 
-  const onPressMenu = () => {
-    track('ViewHeader:MenuButtonClicked')
-    store.shell.openDrawer()
-  }
+    const onPressMenu = () => {
+      track('ViewHeader:MenuButtonClicked')
+      store.shell.openDrawer()
+    }
 
-  const onChangeQuery = (text: string) => {
-    setQuery(text)
-    if (text.length > 0) {
-      autocompleteView.setActive(true)
-      autocompleteView.setPrefix(text)
-    } else {
+    const onChangeQuery = (text: string) => {
+      setQuery(text)
+      if (text.length > 0) {
+        autocompleteView.setActive(true)
+        autocompleteView.setPrefix(text)
+      } else {
+        autocompleteView.setActive(false)
+      }
+    }
+    const onPressClearQuery = () => {
+      setQuery('')
+    }
+    const onPressCancelSearch = () => {
+      setQuery('')
       autocompleteView.setActive(false)
+      textInput.current?.blur()
     }
-  }
-  const onPressClearQuery = () => {
-    setQuery('')
-  }
-  const onPressCancelSearch = () => {
-    setQuery('')
-    autocompleteView.setActive(false)
-    textInput.current?.blur()
-  }
 
-  return (
-    <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
-      <ScrollView
-        ref={scrollElRef}
-        testID="searchScrollView"
-        style={[pal.view, styles.container]}
-        onScroll={onMainScroll}
-        scrollEventThrottle={100}>
-        <View style={[pal.view, pal.border, styles.header]}>
-          <TouchableOpacity
-            testID="viewHeaderBackOrMenuBtn"
-            onPress={onPressMenu}
-            hitSlop={MENU_HITSLOP}
-            style={styles.headerMenuBtn}>
-            <UserAvatar size={30} avatar={store.me.avatar} />
-          </TouchableOpacity>
-          <View
-            style={[
-              {backgroundColor: pal.colors.backgroundLight},
-              styles.headerSearchContainer,
-            ]}>
-            <MagnifyingGlassIcon
-              style={[pal.icon, styles.headerSearchIcon]}
-              size={21}
-            />
-            <TextInput
-              testID="searchTextInput"
-              ref={textInput}
-              placeholder="Search"
-              placeholderTextColor={pal.colors.textLight}
-              selectTextOnFocus
-              returnKeyType="search"
-              value={query}
-              style={[pal.text, styles.headerSearchInput]}
-              keyboardAppearance={theme.colorScheme}
-              onFocus={() => setIsInputFocused(true)}
-              onBlur={() => setIsInputFocused(false)}
-              onChangeText={onChangeQuery}
-            />
-            {query ? (
-              <TouchableOpacity onPress={onPressClearQuery}>
-                <FontAwesomeIcon
-                  icon="xmark"
-                  size={16}
-                  style={pal.textLight as FontAwesomeIconStyle}
-                />
-              </TouchableOpacity>
+    return (
+      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
+        <ScrollView
+          ref={scrollElRef}
+          testID="searchScrollView"
+          style={[pal.view, styles.container]}
+          onScroll={onMainScroll}
+          scrollEventThrottle={100}>
+          <View style={[pal.view, pal.border, styles.header]}>
+            <TouchableOpacity
+              testID="viewHeaderBackOrMenuBtn"
+              onPress={onPressMenu}
+              hitSlop={MENU_HITSLOP}
+              style={styles.headerMenuBtn}>
+              <UserAvatar size={30} avatar={store.me.avatar} />
+            </TouchableOpacity>
+            <View
+              style={[
+                {backgroundColor: pal.colors.backgroundLight},
+                styles.headerSearchContainer,
+              ]}>
+              <MagnifyingGlassIcon
+                style={[pal.icon, styles.headerSearchIcon]}
+                size={21}
+              />
+              <TextInput
+                testID="searchTextInput"
+                ref={textInput}
+                placeholder="Search"
+                placeholderTextColor={pal.colors.textLight}
+                selectTextOnFocus
+                returnKeyType="search"
+                value={query}
+                style={[pal.text, styles.headerSearchInput]}
+                keyboardAppearance={theme.colorScheme}
+                onFocus={() => setIsInputFocused(true)}
+                onBlur={() => setIsInputFocused(false)}
+                onChangeText={onChangeQuery}
+              />
+              {query ? (
+                <TouchableOpacity onPress={onPressClearQuery}>
+                  <FontAwesomeIcon
+                    icon="xmark"
+                    size={16}
+                    style={pal.textLight as FontAwesomeIconStyle}
+                  />
+                </TouchableOpacity>
+              ) : undefined}
+            </View>
+            {query || isInputFocused ? (
+              <View style={styles.headerCancelBtn}>
+                <TouchableOpacity onPress={onPressCancelSearch}>
+                  <Text style={pal.text}>Cancel</Text>
+                </TouchableOpacity>
+              </View>
             ) : undefined}
           </View>
-          {query || isInputFocused ? (
-            <View style={styles.headerCancelBtn}>
-              <TouchableOpacity onPress={onPressCancelSearch}>
-                <Text style={pal.text}>Cancel</Text>
-              </TouchableOpacity>
+          {query && autocompleteView.searchRes.length ? (
+            <>
+              {autocompleteView.searchRes.map(item => (
+                <ProfileCard
+                  key={item.did}
+                  handle={item.handle}
+                  displayName={item.displayName}
+                  avatar={item.avatar}
+                />
+              ))}
+            </>
+          ) : query && !autocompleteView.searchRes.length ? (
+            <View>
+              <Text style={[pal.textLight, styles.searchPrompt]}>
+                No results found for {autocompleteView.prefix}
+              </Text>
             </View>
-          ) : undefined}
-        </View>
-        {query && autocompleteView.searchRes.length ? (
-          <>
-            {autocompleteView.searchRes.map(item => (
-              <ProfileCard
-                key={item.did}
-                handle={item.handle}
-                displayName={item.displayName}
-                avatar={item.avatar}
-              />
-            ))}
-          </>
-        ) : query && !autocompleteView.searchRes.length ? (
-          <View>
-            <Text style={[pal.textLight, styles.searchPrompt]}>
-              No results found for {autocompleteView.prefix}
-            </Text>
-          </View>
-        ) : isInputFocused ? (
-          <View>
-            <Text style={[pal.textLight, styles.searchPrompt]}>
-              Search for users on the network
-            </Text>
-          </View>
-        ) : (
-          <ScrollView onScroll={Keyboard.dismiss}>
-            <WhoToFollow key={`wtf-${lastRenderTime}`} />
-            <SuggestedPosts key={`sp-${lastRenderTime}`} />
-            <View style={s.footerSpacer} />
-          </ScrollView>
-        )}
-        <View style={s.footerSpacer} />
-      </ScrollView>
-    </TouchableWithoutFeedback>
-  )
-})
+          ) : isInputFocused ? (
+            <View>
+              <Text style={[pal.textLight, styles.searchPrompt]}>
+                Search for users on the network
+              </Text>
+            </View>
+          ) : (
+            <ScrollView onScroll={Keyboard.dismiss}>
+              <WhoToFollow key={`wtf-${lastRenderTime}`} />
+              <SuggestedPosts key={`sp-${lastRenderTime}`} />
+              <View style={s.footerSpacer} />
+            </ScrollView>
+          )}
+          <View style={s.footerSpacer} />
+        </ScrollView>
+      </TouchableWithoutFeedback>
+    )
+  }),
+)
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx
index 75b5f01ce..29b884493 100644
--- a/src/view/screens/Search.web.tsx
+++ b/src/view/screens/Search.web.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ScrollView} from '../com/util/Views'
 import {observer} from 'mobx-react-lite'
 import {
@@ -17,46 +18,48 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 const FIVE_MIN = 5 * 60 * 1e3
 
 type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
-export const SearchScreen = observer(({}: Props) => {
-  const pal = usePalette('default')
-  const store = useStores()
-  const scrollElRef = React.useRef<ScrollView>(null)
-  const onMainScroll = useOnMainScroll(store)
-  const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
+export const SearchScreen = withAuthRequired(
+  observer(({}: Props) => {
+    const pal = usePalette('default')
+    const store = useStores()
+    const scrollElRef = React.useRef<ScrollView>(null)
+    const onMainScroll = useOnMainScroll(store)
+    const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
 
-  const onSoftReset = () => {
-    scrollElRef.current?.scrollTo({x: 0, y: 0})
-  }
+    const onSoftReset = () => {
+      scrollElRef.current?.scrollTo({x: 0, y: 0})
+    }
 
-  useFocusEffect(
-    React.useCallback(() => {
-      const softResetSub = store.onScreenSoftReset(onSoftReset)
+    useFocusEffect(
+      React.useCallback(() => {
+        const softResetSub = store.onScreenSoftReset(onSoftReset)
 
-      const now = Date.now()
-      if (now - lastRenderTime > FIVE_MIN) {
-        setRenderTime(Date.now()) // trigger reload of suggestions
-      }
-      store.shell.setMinimalShellMode(false)
+        const now = Date.now()
+        if (now - lastRenderTime > FIVE_MIN) {
+          setRenderTime(Date.now()) // trigger reload of suggestions
+        }
+        store.shell.setMinimalShellMode(false)
 
-      return () => {
-        softResetSub.remove()
-      }
-    }, [store, lastRenderTime, setRenderTime]),
-  )
+        return () => {
+          softResetSub.remove()
+        }
+      }, [store, lastRenderTime, setRenderTime]),
+    )
 
-  return (
-    <ScrollView
-      ref={scrollElRef}
-      testID="searchScrollView"
-      style={[pal.view, styles.container]}
-      onScroll={onMainScroll}
-      scrollEventThrottle={100}>
-      <WhoToFollow key={`wtf-${lastRenderTime}`} />
-      <SuggestedPosts key={`sp-${lastRenderTime}`} />
-      <View style={s.footerSpacer} />
-    </ScrollView>
-  )
-})
+    return (
+      <ScrollView
+        ref={scrollElRef}
+        testID="searchScrollView"
+        style={[pal.view, styles.container]}
+        onScroll={onMainScroll}
+        scrollEventThrottle={100}>
+        <WhoToFollow key={`wtf-${lastRenderTime}`} />
+        <SuggestedPosts key={`sp-${lastRenderTime}`} />
+        <View style={s.footerSpacer} />
+      </ScrollView>
+    )
+  }),
+)
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 2e5d2c001..a79a357b7 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -16,6 +16,7 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import * as AppInfo from 'lib/app-info'
 import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
@@ -33,235 +34,237 @@ import {useAnalytics} from 'lib/analytics'
 import {NavigationProp} from 'lib/routes/types'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
-export const SettingsScreen = observer(function Settings({}: Props) {
-  const theme = useTheme()
-  const pal = usePalette('default')
-  const store = useStores()
-  const navigation = useNavigation<NavigationProp>()
-  const {screen, track} = useAnalytics()
-  const [isSwitching, setIsSwitching] = React.useState(false)
+export const SettingsScreen = withAuthRequired(
+  observer(function Settings({}: Props) {
+    const theme = useTheme()
+    const pal = usePalette('default')
+    const store = useStores()
+    const navigation = useNavigation<NavigationProp>()
+    const {screen, track} = useAnalytics()
+    const [isSwitching, setIsSwitching] = React.useState(false)
 
-  useFocusEffect(
-    React.useCallback(() => {
-      screen('Settings')
-      store.shell.setMinimalShellMode(false)
-    }, [screen, store]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        screen('Settings')
+        store.shell.setMinimalShellMode(false)
+      }, [screen, store]),
+    )
 
-  const onPressSwitchAccount = async (acct: AccountData) => {
-    track('Settings:SwitchAccountButtonClicked')
-    setIsSwitching(true)
-    if (await store.session.resumeSession(acct)) {
+    const onPressSwitchAccount = async (acct: AccountData) => {
+      track('Settings:SwitchAccountButtonClicked')
+      setIsSwitching(true)
+      if (await store.session.resumeSession(acct)) {
+        setIsSwitching(false)
+        navigation.navigate('HomeTab')
+        navigation.dispatch(StackActions.popToTop())
+        Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
+        return
+      }
       setIsSwitching(false)
+      Toast.show('Sorry! We need you to enter your password.')
       navigation.navigate('HomeTab')
       navigation.dispatch(StackActions.popToTop())
-      Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
-      return
+      store.session.clear()
+    }
+    const onPressAddAccount = () => {
+      track('Settings:AddAccountButtonClicked')
+      store.session.clear()
+    }
+    const onPressChangeHandle = () => {
+      track('Settings:ChangeHandleButtonClicked')
+      store.shell.openModal({
+        name: 'change-handle',
+        onChanged() {
+          setIsSwitching(true)
+          store.session.reloadFromServer().then(
+            () => {
+              setIsSwitching(false)
+              Toast.show('Your handle has been updated')
+            },
+            err => {
+              store.log.error(
+                'Failed to reload from server after handle update',
+                {err},
+              )
+              setIsSwitching(false)
+            },
+          )
+        },
+      })
+    }
+    const onPressSignout = () => {
+      track('Settings:SignOutButtonClicked')
+      store.session.logout()
+    }
+    const onPressDeleteAccount = () => {
+      store.shell.openModal({name: 'delete-account'})
     }
-    setIsSwitching(false)
-    Toast.show('Sorry! We need you to enter your password.')
-    navigation.navigate('HomeTab')
-    navigation.dispatch(StackActions.popToTop())
-    store.session.clear()
-  }
-  const onPressAddAccount = () => {
-    track('Settings:AddAccountButtonClicked')
-    store.session.clear()
-  }
-  const onPressChangeHandle = () => {
-    track('Settings:ChangeHandleButtonClicked')
-    store.shell.openModal({
-      name: 'change-handle',
-      onChanged() {
-        setIsSwitching(true)
-        store.session.reloadFromServer().then(
-          () => {
-            setIsSwitching(false)
-            Toast.show('Your handle has been updated')
-          },
-          err => {
-            store.log.error(
-              'Failed to reload from server after handle update',
-              {err},
-            )
-            setIsSwitching(false)
-          },
-        )
-      },
-    })
-  }
-  const onPressSignout = () => {
-    track('Settings:SignOutButtonClicked')
-    store.session.logout()
-  }
-  const onPressDeleteAccount = () => {
-    store.shell.openModal({name: 'delete-account'})
-  }
 
-  return (
-    <View style={[s.hContentRegion]} testID="settingsScreen">
-      <ViewHeader title="Settings" />
-      <ScrollView style={s.hContentRegion}>
-        <View style={styles.spacer20} />
-        <View style={[s.flexRow, styles.heading]}>
-          <Text type="xl-bold" style={pal.text}>
-            Signed in as
-          </Text>
-          <View style={s.flex1} />
-        </View>
-        {isSwitching ? (
-          <View style={[pal.view, styles.linkCard]}>
-            <ActivityIndicator />
+    return (
+      <View style={[s.hContentRegion]} testID="settingsScreen">
+        <ViewHeader title="Settings" />
+        <ScrollView style={s.hContentRegion}>
+          <View style={styles.spacer20} />
+          <View style={[s.flexRow, styles.heading]}>
+            <Text type="xl-bold" style={pal.text}>
+              Signed in as
+            </Text>
+            <View style={s.flex1} />
           </View>
-        ) : (
-          <Link
-            href={`/profile/${store.me.handle}`}
-            title="Your profile"
-            noFeedback>
+          {isSwitching ? (
             <View style={[pal.view, styles.linkCard]}>
+              <ActivityIndicator />
+            </View>
+          ) : (
+            <Link
+              href={`/profile/${store.me.handle}`}
+              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}>
+                  <Text type="lg" style={pal.link}>
+                    Sign out
+                  </Text>
+                </TouchableOpacity>
+              </View>
+            </Link>
+          )}
+          {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)
+              }>
               <View style={styles.avi}>
-                <UserAvatar size={40} avatar={store.me.avatar} />
+                <UserAvatar size={40} avatar={account.aviUrl} />
               </View>
               <View style={[s.flex1]}>
-                <Text type="md-bold" style={pal.text} numberOfLines={1}>
-                  {store.me.displayName || store.me.handle}
+                <Text type="md-bold" style={pal.text}>
+                  {account.displayName || account.handle}
                 </Text>
-                <Text type="sm" style={pal.textLight} numberOfLines={1}>
-                  {store.me.handle}
+                <Text type="sm" style={pal.textLight}>
+                  {account.handle}
                 </Text>
               </View>
-              <TouchableOpacity
-                testID="signOutBtn"
-                onPress={isSwitching ? undefined : onPressSignout}>
-                <Text type="lg" style={pal.link}>
-                  Sign out
-                </Text>
-              </TouchableOpacity>
-            </View>
-          </Link>
-        )}
-        {store.session.switchableAccounts.map(account => (
+              <AccountDropdownBtn handle={account.handle} />
+            </TouchableOpacity>
+          ))}
           <TouchableOpacity
-            testID={`switchToAccountBtn-${account.handle}`}
-            key={account.did}
-            style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
-            onPress={
-              isSwitching ? undefined : () => onPressSwitchAccount(account)
-            }>
-            <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>
+            testID="switchToNewAccountBtn"
+            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            onPress={isSwitching ? undefined : onPressAddAccount}>
+            <View style={[styles.iconContainer, pal.btn]}>
+              <FontAwesomeIcon
+                icon="plus"
+                style={pal.text as FontAwesomeIconStyle}
+              />
             </View>
-            <AccountDropdownBtn handle={account.handle} />
+            <Text type="lg" style={pal.text}>
+              Add account
+            </Text>
           </TouchableOpacity>
-        ))}
-        <TouchableOpacity
-          testID="switchToNewAccountBtn"
-          style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-          onPress={isSwitching ? undefined : onPressAddAccount}>
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="plus"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            Add account
-          </Text>
-        </TouchableOpacity>
 
-        <View style={styles.spacer20} />
+          <View style={styles.spacer20} />
 
-        <Text type="xl-bold" style={[pal.text, styles.heading]}>
-          Advanced
-        </Text>
-        <TouchableOpacity
-          testID="changeHandleBtn"
-          style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-          onPress={isSwitching ? undefined : onPressChangeHandle}>
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="at"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            Change my handle
+          <Text type="xl-bold" style={[pal.text, styles.heading]}>
+            Advanced
           </Text>
-        </TouchableOpacity>
+          <TouchableOpacity
+            testID="changeHandleBtn"
+            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
+            onPress={isSwitching ? undefined : onPressChangeHandle}>
+            <View style={[styles.iconContainer, pal.btn]}>
+              <FontAwesomeIcon
+                icon="at"
+                style={pal.text as FontAwesomeIconStyle}
+              />
+            </View>
+            <Text type="lg" style={pal.text}>
+              Change my handle
+            </Text>
+          </TouchableOpacity>
 
-        <View style={styles.spacer20} />
+          <View style={styles.spacer20} />
 
-        <Text type="xl-bold" style={[pal.text, styles.heading]}>
-          Danger zone
-        </Text>
-        <TouchableOpacity
-          style={[pal.view, styles.linkCard]}
-          onPress={onPressDeleteAccount}>
-          <View
-            style={[
-              styles.iconContainer,
-              theme.colorScheme === 'dark'
-                ? styles.trashIconContainerDark
-                : styles.trashIconContainerLight,
-            ]}>
-            <FontAwesomeIcon
-              icon={['far', 'trash-can']}
+          <Text type="xl-bold" style={[pal.text, styles.heading]}>
+            Danger zone
+          </Text>
+          <TouchableOpacity
+            style={[pal.view, styles.linkCard]}
+            onPress={onPressDeleteAccount}>
+            <View
+              style={[
+                styles.iconContainer,
+                theme.colorScheme === 'dark'
+                  ? styles.trashIconContainerDark
+                  : styles.trashIconContainerLight,
+              ]}>
+              <FontAwesomeIcon
+                icon={['far', 'trash-can']}
+                style={
+                  theme.colorScheme === 'dark'
+                    ? styles.dangerDark
+                    : styles.dangerLight
+                }
+                size={21}
+              />
+            </View>
+            <Text
+              type="lg"
               style={
                 theme.colorScheme === 'dark'
                   ? styles.dangerDark
                   : styles.dangerLight
-              }
-              size={21}
-            />
-          </View>
-          <Text
-            type="lg"
-            style={
-              theme.colorScheme === 'dark'
-                ? styles.dangerDark
-                : styles.dangerLight
-            }>
-            Delete my account
-          </Text>
-        </TouchableOpacity>
+              }>
+              Delete my account
+            </Text>
+          </TouchableOpacity>
 
-        <View style={styles.spacer20} />
+          <View style={styles.spacer20} />
 
-        <Text type="xl-bold" style={[pal.text, styles.heading]}>
-          Developer tools
-        </Text>
-        <Link
-          style={[pal.view, styles.linkCardNoIcon]}
-          href="/sys/log"
-          title="System log">
-          <Text type="lg" style={pal.text}>
-            System log
+          <Text type="xl-bold" style={[pal.text, styles.heading]}>
+            Developer tools
           </Text>
-        </Link>
-        <Link
-          style={[pal.view, styles.linkCardNoIcon]}
-          href="/sys/debug"
-          title="Debug tools">
-          <Text type="lg" style={pal.text}>
-            Storybook
+          <Link
+            style={[pal.view, styles.linkCardNoIcon]}
+            href="/sys/log"
+            title="System log">
+            <Text type="lg" style={pal.text}>
+              System log
+            </Text>
+          </Link>
+          <Link
+            style={[pal.view, styles.linkCardNoIcon]}
+            href="/sys/debug"
+            title="Debug tools">
+            <Text type="lg" style={pal.text}>
+              Storybook
+            </Text>
+          </Link>
+          <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
+            Build version {AppInfo.appVersion} ({AppInfo.buildVersion})
           </Text>
-        </Link>
-        <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-          Build version {AppInfo.appVersion} ({AppInfo.buildVersion})
-        </Text>
-        <View style={s.footerSpacer} />
-      </ScrollView>
-    </View>
-  )
-})
+          <View style={s.footerSpacer} />
+        </ScrollView>
+      </View>
+    )
+  }),
+)
 
 function AccountDropdownBtn({handle}: {handle: string}) {
   const store = useStores()
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 46c77178b..65757b072 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -131,7 +131,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
 
   return (
     <View style={styles.leftNav}>
-      <ProfileCard />
+      {store.session.hasSession && <ProfileCard />}
       <BackBtn />
       <NavItem
         href="/"
@@ -164,14 +164,16 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         }
         label="Notifications"
       />
-      <NavItem
-        href={`/profile/${store.me.handle}`}
-        icon={<UserIcon strokeWidth={1.75} size={28} style={pal.text} />}
-        iconFilled={
-          <UserIconSolid strokeWidth={1.75} size={28} style={pal.text} />
-        }
-        label="Profile"
-      />
+      {store.session.hasSession && (
+        <NavItem
+          href={`/profile/${store.me.handle}`}
+          icon={<UserIcon strokeWidth={1.75} size={28} style={pal.text} />}
+          iconFilled={
+            <UserIconSolid strokeWidth={1.75} size={28} style={pal.text} />
+          }
+          label="Profile"
+        />
+      )}
       <NavItem
         href="/settings"
         icon={<CogIcon strokeWidth={1.75} size={28} style={pal.text} />}
@@ -180,7 +182,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         }
         label="Settings"
       />
-      <ComposeBtn />
+      {store.session.hasSession && <ComposeBtn />}
     </View>
   )
 })
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 58fb31392..3f196cb70 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -7,12 +7,14 @@ import {Text} from 'view/com/util/text/Text'
 import {TextLink} from 'view/com/util/Link'
 import {FEEDBACK_FORM_URL} from 'lib/constants'
 import {s} from 'lib/styles'
+import {useStores} from 'state/index'
 
 export const DesktopRightNav = observer(function DesktopRightNav() {
+  const store = useStores()
   const pal = usePalette('default')
   return (
     <View style={[styles.rightNav, pal.view]}>
-      <DesktopSearch />
+      {store.session.hasSession && <DesktopSearch />}
       <View style={styles.message}>
         <Text type="md" style={[pal.textLight, styles.messageLine]}>
           Welcome to Bluesky! This is a beta application that's still in
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 116915ff4..15f9ef58c 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -5,7 +5,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {Drawer} from 'react-native-drawer-layout'
 import {useNavigationState} from '@react-navigation/native'
 import {useStores} from 'state/index'
-import {Login} from 'view/screens/Login'
 import {ModalsContainer} from 'view/com/modals/Modal'
 import {Lightbox} from 'view/com/lightbox/Lightbox'
 import {Text} from 'view/com/util/text/Text'
@@ -104,20 +103,6 @@ export const Shell: React.FC = observer(() => {
     )
   }
 
-  if (!store.session.hasSession) {
-    return (
-      <View style={styles.outerContainer}>
-        <StatusBar
-          barStyle={
-            theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
-          }
-        />
-        <Login />
-        <ModalsContainer />
-      </View>
-    )
-  }
-
   return (
     <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
       <StatusBar
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 9a97505e8..a588c99a1 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -4,7 +4,6 @@ import {View, StyleSheet} from 'react-native'
 import {useStores} from 'state/index'
 import {DesktopLeftNav} from './desktop/LeftNav'
 import {DesktopRightNav} from './desktop/RightNav'
-import {Login} from '../screens/Login'
 import {ErrorBoundary} from '../com/util/ErrorBoundary'
 import {Lightbox} from '../com/lightbox/Lightbox'
 import {ModalsContainer} from '../com/modals/Modal'
@@ -45,21 +44,10 @@ const ShellInner = observer(() => {
 
 export const Shell: React.FC = observer(() => {
   const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
-  const store = useStores()
 
   if (isMobileWeb) {
     return <NoMobileWeb />
   }
-
-  if (!store.session.hasSession) {
-    return (
-      <View style={[s.hContentRegion, pageBg]}>
-        <Login />
-        <ModalsContainer />
-      </View>
-    )
-  }
-
   return (
     <View style={[s.hContentRegion, pageBg]}>
       <RoutesContainer>