about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Navigation.tsx23
-rw-r--r--src/lib/analytics/types.ts1
-rw-r--r--src/lib/routes/types.ts5
-rw-r--r--src/state/models/discovery/onboarding.ts82
-rw-r--r--src/state/models/root-store.ts6
-rw-r--r--src/view/com/auth/onboarding/Onboarding.tsx66
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeeds.tsx192
-rw-r--r--src/view/com/auth/onboarding/Welcome.tsx53
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/modals/OnboardingModal.tsx8
-rw-r--r--src/view/index.ts2
-rw-r--r--src/view/screens/Home.tsx8
-rw-r--r--src/view/screens/Settings.tsx15
14 files changed, 384 insertions, 84 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 48bab182d..058a15fa2 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -67,6 +67,8 @@ import {getRoutingInstrumentation} from 'lib/sentry'
 import {bskyTitle} from 'lib/strings/headings'
 import {JSX} from 'react/jsx-runtime'
 import {timeout} from 'lib/async/timeout'
+import {Welcome, WelcomeHeaderRight} from 'view/com/auth/onboarding/Welcome'
+import {RecommendedFeeds} from 'view/com/auth/onboarding/RecommendedFeeds'
 
 const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
 
@@ -219,6 +221,26 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
         component={SavedFeeds}
         options={{title: title('Edit My Feeds')}}
       />
+      <Stack.Screen
+        name="Welcome"
+        component={Welcome}
+        options={{
+          title: title('Welcome'),
+          presentation: 'card',
+          headerShown: true,
+          headerTransparent: true,
+          headerTitle: '',
+          headerBackVisible: false,
+          headerRight: props => <WelcomeHeaderRight {...props} />,
+        }}
+      />
+      <Stack.Screen
+        name="RecommendedFeeds"
+        component={RecommendedFeeds}
+        options={{
+          title: title('Recommended Feeds'),
+        }}
+      />
     </>
   )
 }
@@ -254,6 +276,7 @@ function TabsNavigator() {
 
 function HomeTabNavigator() {
   const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
+
   return (
     <HomeTab.Navigator
       screenOptions={{
diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts
index f876c6d53..d56e1b615 100644
--- a/src/lib/analytics/types.ts
+++ b/src/lib/analytics/types.ts
@@ -122,6 +122,7 @@ interface TrackPropertiesMap {
   // ONBOARDING events
   'Onboarding:Begin': {}
   'Onboarding:Complete': {}
+  'Onboarding:Skipped': {}
 }
 
 interface ScreenPropertiesMap {
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 4eb5e29d2..633fa57a5 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -1,5 +1,6 @@
 import {NavigationState, PartialState} from '@react-navigation/native'
 import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
+import {OnboardingScreenSteps} from 'state/models/discovery/onboarding'
 
 export type {NativeStackScreenProps} from '@react-navigation/native-stack'
 
@@ -29,6 +30,10 @@ export type CommonNavigatorParams = {
   CopyrightPolicy: undefined
   AppPasswords: undefined
   SavedFeeds: undefined
+} & OnboardingScreenParams
+
+export type OnboardingScreenParams = {
+  [K in keyof typeof OnboardingScreenSteps]: undefined
 }
 
 export type BottomTabNavigatorParams = CommonNavigatorParams & {
diff --git a/src/state/models/discovery/onboarding.ts b/src/state/models/discovery/onboarding.ts
new file mode 100644
index 000000000..9b49beaf4
--- /dev/null
+++ b/src/state/models/discovery/onboarding.ts
@@ -0,0 +1,82 @@
+import {makeAutoObservable} from 'mobx'
+import {RootStoreModel} from '../root-store'
+import {hasProp} from 'lib/type-guards'
+import {track} from 'lib/analytics/analytics'
+
+export const OnboardingScreenSteps = {
+  Welcome: 'Welcome',
+  RecommendedFeeds: 'RecommendedFeeds',
+  Home: 'Home',
+} as const
+
+type OnboardingStep =
+  (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
+const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
+export class OnboardingModel {
+  // state
+  step: OnboardingStep = 'Welcome'
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(this, {
+      rootStore: false,
+      hydrate: false,
+      serialize: false,
+    })
+  }
+
+  serialize(): unknown {
+    console.log('serializing onboarding', this.step)
+    return {
+      step: this.step,
+    }
+  }
+
+  hydrate(v: unknown) {
+    if (typeof v === 'object' && v !== null) {
+      if (
+        hasProp(v, 'step') &&
+        typeof v.step === 'string' &&
+        OnboardingStepsArray.includes(v.step as OnboardingStep)
+      ) {
+        console.log('hydrating onboarding', v.step)
+        this.step = v.step as OnboardingStep
+      }
+    } else {
+      // if there is no valid state, we'll just reset
+      this.reset()
+    }
+  }
+
+  nextScreenName(currentScreenName?: OnboardingStep) {
+    if (currentScreenName === 'Welcome' || this.step === 'Welcome') {
+      this.step = 'RecommendedFeeds'
+      return this.step
+    } else if (
+      this.step === 'RecommendedFeeds' ||
+      currentScreenName === 'RecommendedFeeds'
+    ) {
+      this.step = 'Home'
+      return this.step
+    } else {
+      // if we get here, we're in an invalid state, let's just go Home
+      return 'Home'
+    }
+  }
+
+  reset() {
+    this.step = 'Welcome'
+  }
+
+  skip() {
+    track('Onboarding:Skipped')
+    this.step = 'Home'
+  }
+
+  get isComplete() {
+    return this.step === 'Home'
+  }
+
+  get isRemaining() {
+    return !this.isComplete
+  }
+}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 1d6d3a0d0..6204e0d10 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -27,6 +27,7 @@ import {reset as resetNavigation} from '../../Navigation'
 // remove after backend testing finishes
 // -prf
 import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
+import {OnboardingModel} from './discovery/onboarding'
 
 export const appInfo = z.object({
   build: z.string(),
@@ -44,6 +45,7 @@ export class RootStoreModel {
   shell = new ShellUiModel(this)
   preferences = new PreferencesModel(this)
   me = new MeModel(this)
+  onboarding = new OnboardingModel(this)
   invitedUsers = new InvitedUsers(this)
   handleResolutions = new HandleResolutionsCache()
   profiles = new ProfilesCache(this)
@@ -70,6 +72,7 @@ export class RootStoreModel {
       appInfo: this.appInfo,
       session: this.session.serialize(),
       me: this.me.serialize(),
+      onboarding: this.onboarding.serialize(),
       shell: this.shell.serialize(),
       preferences: this.preferences.serialize(),
       invitedUsers: this.invitedUsers.serialize(),
@@ -88,6 +91,9 @@ export class RootStoreModel {
       if (hasProp(v, 'me')) {
         this.me.hydrate(v.me)
       }
+      if (hasProp(v, 'onboarding')) {
+        this.onboarding.hydrate(v.onboarding)
+      }
       if (hasProp(v, 'session')) {
         this.session.hydrate(v.session)
       }
diff --git a/src/view/com/auth/onboarding/Onboarding.tsx b/src/view/com/auth/onboarding/Onboarding.tsx
deleted file mode 100644
index 28e4419d7..000000000
--- a/src/view/com/auth/onboarding/Onboarding.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Welcome} from './Welcome'
-import {useStores} from 'state/index'
-import {track} from 'lib/analytics/analytics'
-
-enum OnboardingStep {
-  WELCOME = 'WELCOME',
-  // SELECT_INTERESTS = 'SELECT_INTERESTS',
-  COMPLETE = 'COMPLETE',
-}
-type OnboardingState = {
-  currentStep: OnboardingStep
-}
-type Action = {type: 'NEXT_STEP'}
-const initialState: OnboardingState = {
-  currentStep: OnboardingStep.WELCOME,
-}
-const reducer = (state: OnboardingState, action: Action): OnboardingState => {
-  switch (action.type) {
-    case 'NEXT_STEP':
-      switch (state.currentStep) {
-        case OnboardingStep.WELCOME:
-          track('Onboarding:Begin')
-          return {...state, currentStep: OnboardingStep.COMPLETE}
-        case OnboardingStep.COMPLETE:
-          track('Onboarding:Complete')
-          return state
-        default:
-          return state
-      }
-    default:
-      return state
-  }
-}
-
-export const Onboarding = () => {
-  const pal = usePalette('default')
-  const rootStore = useStores()
-  const [state, dispatch] = React.useReducer(reducer, initialState)
-  const next = React.useCallback(
-    () => dispatch({type: 'NEXT_STEP'}),
-    [dispatch],
-  )
-
-  React.useEffect(() => {
-    if (state.currentStep === OnboardingStep.COMPLETE) {
-      // navigate to home
-      rootStore.shell.closeModal()
-    }
-  }, [state.currentStep, rootStore.shell])
-
-  return (
-    <View style={[pal.view, styles.container]}>
-      {state.currentStep === OnboardingStep.WELCOME && <Welcome next={next} />}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingHorizontal: 20,
-  },
-})
diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
new file mode 100644
index 000000000..88fb200c6
--- /dev/null
+++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
@@ -0,0 +1,192 @@
+import React from 'react'
+import {FlatList, StyleSheet, View} from 'react-native'
+import {Text} from 'view/com/util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {Button} from 'view/com/util/forms/Button'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {HomeTabNavigatorParams} from 'lib/routes/types'
+import {useStores} from 'state/index'
+import {observer} from 'mobx-react-lite'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+import {useCustomFeed} from 'lib/hooks/useCustomFeed'
+import {makeRecordUri} from 'lib/strings/url-helpers'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+
+const TEMPORARY_RECOMMENDED_FEEDS = [
+  {
+    did: 'did:plc:hsqwcidfez66lwm3gxhfv5in',
+    rkey: 'aaaf2pqeodmpy',
+  },
+  {
+    did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn',
+    rkey: 'aaap4tbjcfe5y',
+  },
+  {
+    did: 'did:plc:5rw2on4i56btlcajojaxwcat',
+    rkey: 'aaao6g552b33o',
+  },
+  {
+    did: 'did:plc:jfhpnnst6flqway4eaeqzj2a',
+    rkey: 'for-science',
+  },
+  {
+    did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy',
+    rkey: 'bsky-news',
+  },
+  {
+    did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25',
+    rkey: 'astro',
+  },
+  {
+    did: 'did:plc:tenurhgjptubkk5zf5qhi3og',
+    rkey: 'h-nba',
+  },
+  {
+    did: 'did:plc:vpkhqolt662uhesyj6nxm7ys',
+    rkey: 'devfeed',
+  },
+  {
+    did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5',
+    rkey: 'flipboard-tech',
+  },
+  {
+    did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw',
+    rkey: 'blacksky',
+  },
+  {
+    did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe',
+    rkey: 'aaaotfjzjplna',
+  },
+  {
+    did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z',
+    rkey: 'formula-one',
+  },
+  {
+    did: 'did:plc:q6gjnaw2blty4crticxkmujt',
+    rkey: 'positivifeed',
+  },
+  {
+    did: 'did:plc:l72uci4styb4jucsgcrrj5ap',
+    rkey: 'aaao5dzfm36u4',
+  },
+  {
+    did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n',
+    rkey: 'aaaavlyvqzst2',
+  },
+  {
+    did: 'did:plc:nkahctfdi6bxk72umytfwghw',
+    rkey: 'aaado2uvfsc6w',
+  },
+  {
+    did: 'did:plc:epihigio3d7un7u3gpqiy5gv',
+    rkey: 'aaaekwsc7zsvs',
+  },
+  {
+    did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7',
+    rkey: 'aaaejxlobe474',
+  },
+  {
+    did: 'did:plc:mlq4aycufcuolr7ax6sezpc4',
+    rkey: 'aaaoudweck6uy',
+  },
+  {
+    did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg',
+    rkey: 'aaadzjxbcddzi',
+  },
+  {
+    did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr',
+    rkey: 'aaab2vesjtszc',
+  },
+  {
+    did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if',
+    rkey: 'aaacynbxwimok',
+  },
+  {
+    did: 'did:plc:abv47bjgzjgoh3yrygwoi36x',
+    rkey: 'aaagt6amuur5e',
+  },
+  {
+    did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt',
+    rkey: 'aaacjerk7gwek',
+  },
+  {
+    did: 'did:plc:geoqe3qls5mwezckxxsewys2',
+    rkey: 'aaai43yetqshu',
+  },
+  {
+    did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34',
+    rkey: 'authors',
+  },
+]
+
+type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'RecommendedFeeds'>
+export const RecommendedFeeds = observer(({navigation}: Props) => {
+  const pal = usePalette('default')
+  const store = useStores()
+
+  const next = () => {
+    const nextScreenName = store.onboarding.nextScreenName('RecommendedFeeds')
+    if (nextScreenName) {
+      navigation.navigate(nextScreenName)
+    }
+  }
+
+  return (
+    <View style={[styles.container]} testID="recommendedFeedsScreen">
+      <ViewHeader title="Recommended Feeds" canGoBack />
+      <Text type="lg-medium" style={[pal.text, styles.header]}>
+        Check out some recommended feeds. Click + to add them to your list of
+        pinned feeds.
+      </Text>
+
+      <FlatList
+        data={TEMPORARY_RECOMMENDED_FEEDS}
+        renderItem={({item}) => <Item item={item} />}
+        keyExtractor={item => item.did + item.rkey}
+        style={{flex: 1}}
+      />
+
+      <Button
+        onPress={next}
+        label="Continue"
+        testID="continueBtn"
+        style={styles.button}
+        labelStyle={styles.buttonText}
+      />
+    </View>
+  )
+})
+
+type ItemProps = {
+  did: string
+  rkey: string
+}
+
+const Item = ({item}: {item: ItemProps}) => {
+  const uri = makeRecordUri(item.did, 'app.bsky.feed.generator', item.rkey)
+  const data = useCustomFeed(uri)
+  if (!data) return null
+  return (
+    <CustomFeed item={data} key={uri} showDescription showLikes showSaveBtn />
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    marginHorizontal: 16,
+    justifyContent: 'space-between',
+  },
+  header: {
+    marginBottom: 16,
+  },
+  button: {
+    marginBottom: 48,
+    marginTop: 16,
+  },
+  buttonText: {
+    textAlign: 'center',
+    fontSize: 18,
+    paddingVertical: 4,
+  },
+})
diff --git a/src/view/com/auth/onboarding/Welcome.tsx b/src/view/com/auth/onboarding/Welcome.tsx
index 87435c88a..cb3a2307a 100644
--- a/src/view/com/auth/onboarding/Welcome.tsx
+++ b/src/view/com/auth/onboarding/Welcome.tsx
@@ -1,13 +1,36 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {Pressable, StyleSheet, View} from 'react-native'
 import {Text} from 'view/com/util/text/Text'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Button} from 'view/com/util/forms/Button'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {HomeTabNavigatorParams} from 'lib/routes/types'
+import {useStores} from 'state/index'
+import {observer} from 'mobx-react-lite'
+import {HeaderButtonProps} from '@react-navigation/native-stack/lib/typescript/src/types'
+import {NavigationProp, useNavigation} from '@react-navigation/native'
 
-export const Welcome = ({next}: {next: () => void}) => {
+type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Welcome'>
+export const Welcome = observer(({navigation}: Props) => {
   const pal = usePalette('default')
+  const store = useStores()
+
+  // make sure bottom nav is hidden
+  React.useEffect(() => {
+    if (!store.shell.minimalShellMode) {
+      store.shell.setMinimalShellMode(true)
+    }
+  }, [store.shell.minimalShellMode, store])
+
+  const next = () => {
+    const nextScreenName = store.onboarding.nextScreenName('Welcome')
+    if (nextScreenName) {
+      navigation.navigate(nextScreenName)
+    }
+  }
+
   return (
     <View style={[styles.container]}>
       <View testID="welcomeScreen">
@@ -60,12 +83,38 @@ export const Welcome = ({next}: {next: () => void}) => {
       />
     </View>
   )
+})
+
+export const WelcomeHeaderRight = (props: HeaderButtonProps) => {
+  const {canGoBack} = props
+  const pal = usePalette('default')
+  const navigation = useNavigation<NavigationProp<HomeTabNavigatorParams>>()
+  const store = useStores()
+  return (
+    <Pressable
+      accessibilityRole="button"
+      style={[s.flexRow, s.alignCenter]}
+      onPress={() => {
+        if (canGoBack) {
+          store.onboarding.skip()
+          navigation.goBack()
+        }
+      }}>
+      <Text style={[pal.link]}>Skip</Text>
+      <FontAwesomeIcon
+        icon={'chevron-right'}
+        size={14}
+        color={pal.colors.link}
+      />
+    </Pressable>
+  )
 }
 
 const styles = StyleSheet.create({
   container: {
     flex: 1,
     marginVertical: 60,
+    marginHorizontal: 16,
     justifyContent: 'space-between',
   },
   title: {
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index efd06412d..dd45262be 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -29,7 +29,6 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
 import * as PreferencesHomeFeed from './PreferencesHomeFeed'
-import * as OnboardingModal from './OnboardingModal'
 import * as ModerationDetailsModal from './ModerationDetails'
 
 const DEFAULT_SNAPPOINTS = ['90%']
@@ -134,9 +133,6 @@ export const ModalsContainer = observer(function ModalsContainer() {
   } else if (activeModal?.name === 'preferences-home-feed') {
     snapPoints = PreferencesHomeFeed.snapPoints
     element = <PreferencesHomeFeed.Component />
-  } else if (activeModal?.name === 'onboarding') {
-    snapPoints = OnboardingModal.snapPoints
-    element = <OnboardingModal.Component />
   } else if (activeModal?.name === 'moderation-details') {
     snapPoints = ModerationDetailsModal.snapPoints
     element = <ModerationDetailsModal.Component {...activeModal} />
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 687c4fba3..6aef1b71c 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
 import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
-import * as OnboardingModal from './OnboardingModal'
 import * as ModerationDetailsModal from './ModerationDetails'
 
 import * as PreferencesHomeFeed from './PreferencesHomeFeed'
@@ -109,8 +108,6 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <EditImageModal.Component {...modal} />
   } else if (modal.name === 'preferences-home-feed') {
     element = <PreferencesHomeFeed.Component />
-  } else if (modal.name === 'onboarding') {
-    element = <OnboardingModal.Component />
   } else if (modal.name === 'moderation-details') {
     element = <ModerationDetailsModal.Component {...modal} />
   } else {
diff --git a/src/view/com/modals/OnboardingModal.tsx b/src/view/com/modals/OnboardingModal.tsx
deleted file mode 100644
index c70f4fd62..000000000
--- a/src/view/com/modals/OnboardingModal.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react'
-import {Onboarding} from '../auth/onboarding/Onboarding'
-
-export const snapPoints = ['90%']
-
-export function Component() {
-  return <Onboarding />
-}
diff --git a/src/view/index.ts b/src/view/index.ts
index 1c3dc3937..2e4c08ec7 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -92,6 +92,7 @@ import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
 import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
 import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
 import {faList} from '@fortawesome/free-solid-svg-icons/faList'
+import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'
 
 export function setup() {
   library.add(
@@ -187,5 +188,6 @@ export function setup() {
     faPlay,
     faPause,
     faList,
+    faChevronRight,
   )
 }
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 9259d4bea..f2aa208c3 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -31,7 +31,7 @@ const POLL_FREQ = 30e3 // 30sec
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export const HomeScreen = withAuthRequired(
-  observer((_opts: Props) => {
+  observer(({navigation}: Props) => {
     const store = useStores()
     const pagerRef = React.useRef<PagerRef>(null)
     const [selectedPage, setSelectedPage] = React.useState(0)
@@ -41,6 +41,12 @@ export const HomeScreen = withAuthRequired(
     >([])
 
     React.useEffect(() => {
+      if (store.onboarding.isRemaining) {
+        navigation.navigate('Welcome')
+      }
+    }, [store.onboarding.isRemaining, navigation])
+
+    React.useEffect(() => {
       const {pinned} = store.me.savedFeeds
 
       if (
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index f1d4767f3..4a2c1c16a 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -162,6 +162,11 @@ export const SettingsScreen = withAuthRequired(
       Toast.show('Preferences reset')
     }, [store])
 
+    const onPressResetOnboarding = React.useCallback(async () => {
+      store.onboarding.reset()
+      Toast.show('Onboarding reset')
+    }, [store])
+
     const onPressBuildInfo = React.useCallback(() => {
       Clipboard.setString(
         `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
@@ -535,6 +540,16 @@ export const SettingsScreen = withAuthRequired(
                   Reset preferences state
                 </Text>
               </TouchableOpacity>
+              <TouchableOpacity
+                style={[pal.view, styles.linkCardNoIcon]}
+                onPress={onPressResetOnboarding}
+                accessibilityRole="button"
+                accessibilityHint="Reset onboarding"
+                accessibilityLabel="Resets the onboarding state">
+                <Text type="lg" style={pal.text}>
+                  Reset onboarding state
+                </Text>
+              </TouchableOpacity>
             </>
           ) : null}
           <View style={[styles.footer]}>