about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-11-08 09:04:06 -0800
committerGitHub <noreply@github.com>2023-11-08 09:04:06 -0800
commit4afed4be281b6319c328938e4ed757624a78b13c (patch)
tree7a7744801c2964a3981c3e3ed1772f8226276c6b /src
parent3a211017d3d972fb442069e38d1b8ff1a2edbd57 (diff)
downloadvoidsky-4afed4be281b6319c328938e4ed757624a78b13c.tar.zst
Move onboarding state to new persistence + reducer context (#1835)
Diffstat (limited to 'src')
-rw-r--r--src/state/models/discovery/onboarding.ts106
-rw-r--r--src/state/models/root-store.ts6
-rw-r--r--src/state/models/ui/create-account.ts7
-rw-r--r--src/state/persisted/schema.ts6
-rw-r--r--src/state/shell/color-mode.tsx2
-rw-r--r--src/state/shell/index.tsx6
-rw-r--r--src/state/shell/onboarding.tsx119
-rw-r--r--src/view/com/auth/Onboarding.tsx15
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx6
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollows.tsx39
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx33
-rw-r--r--src/view/com/auth/withAuthRequired.tsx4
-rw-r--r--src/view/screens/Settings.tsx6
-rw-r--r--src/view/shell/index.web.tsx11
14 files changed, 199 insertions, 167 deletions
diff --git a/src/state/models/discovery/onboarding.ts b/src/state/models/discovery/onboarding.ts
deleted file mode 100644
index 3638e7f0d..000000000
--- a/src/state/models/discovery/onboarding.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {RootStoreModel} from '../root-store'
-import {hasProp} from 'lib/type-guards'
-import {track} from 'lib/analytics/analytics'
-import {SuggestedActorsModel} from './suggested-actors'
-
-export const OnboardingScreenSteps = {
-  Welcome: 'Welcome',
-  RecommendedFeeds: 'RecommendedFeeds',
-  RecommendedFollows: 'RecommendedFollows',
-  Home: 'Home',
-} as const
-
-type OnboardingStep =
-  (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
-const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
-export class OnboardingModel {
-  // state
-  step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
-
-  // data
-  suggestedActors: SuggestedActorsModel
-
-  constructor(public rootStore: RootStoreModel) {
-    this.suggestedActors = new SuggestedActorsModel(this.rootStore)
-    makeAutoObservable(this, {
-      rootStore: false,
-      hydrate: false,
-      serialize: false,
-    })
-  }
-
-  serialize(): unknown {
-    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)
-      ) {
-        this.step = v.step as OnboardingStep
-      }
-    } else {
-      // if there is no valid state, we'll just reset
-      this.reset()
-    }
-  }
-
-  /**
-   * Returns the name of the next screen in the onboarding process based on the current step or screen name provided.
-   * @param {OnboardingStep} [currentScreenName]
-   * @returns name of next screen in the onboarding process
-   */
-  next(currentScreenName?: OnboardingStep) {
-    currentScreenName = currentScreenName || this.step
-    if (currentScreenName === 'Welcome') {
-      this.step = 'RecommendedFeeds'
-      return this.step
-    } else if (this.step === 'RecommendedFeeds') {
-      this.step = 'RecommendedFollows'
-      // prefetch recommended follows
-      this.suggestedActors.loadMore(true)
-      return this.step
-    } else if (this.step === 'RecommendedFollows') {
-      this.finish()
-      return this.step
-    } else {
-      // if we get here, we're in an invalid state, let's just go Home
-      return 'Home'
-    }
-  }
-
-  start() {
-    this.step = 'Welcome'
-    track('Onboarding:Begin')
-  }
-
-  finish() {
-    this.rootStore.me.mainFeed.refresh() // load the selected content
-    this.step = 'Home'
-    track('Onboarding:Complete')
-  }
-
-  reset() {
-    this.step = 'Welcome'
-    track('Onboarding:Reset')
-  }
-
-  skip() {
-    this.step = 'Home'
-    track('Onboarding:Skipped')
-  }
-
-  get isComplete() {
-    return this.step === 'Home'
-  }
-
-  get isActive() {
-    return !this.isComplete
-  }
-}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 6ba78e711..f04a9922d 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -27,7 +27,6 @@ import {logger} from '#/logger'
 // 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,7 +43,6 @@ 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)
@@ -71,7 +69,6 @@ export class RootStoreModel {
       appInfo: this.appInfo,
       session: this.session.serialize(),
       me: this.me.serialize(),
-      onboarding: this.onboarding.serialize(),
       preferences: this.preferences.serialize(),
       invitedUsers: this.invitedUsers.serialize(),
       mutedThreads: this.mutedThreads.serialize(),
@@ -89,9 +86,6 @@ 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/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index 1711b530f..39c881db6 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -9,6 +9,7 @@ import {cleanError} from 'lib/strings/errors'
 import {getAge} from 'lib/strings/time'
 import {track} from 'lib/analytics/analytics'
 import {logger} from '#/logger'
+import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
 
 const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
 
@@ -90,7 +91,7 @@ export class CreateAccountModel {
     }
   }
 
-  async submit() {
+  async submit(onboardingDispatch: OnboardingDispatchContext) {
     if (!this.email) {
       this.setStep(2)
       return this.setError('Please enter your email.')
@@ -111,7 +112,7 @@ export class CreateAccountModel {
     this.setIsProcessing(true)
 
     try {
-      this.rootStore.onboarding.start() // start now to avoid flashing the wrong view
+      onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
       await this.rootStore.session.createAccount({
         service: this.serviceUrl,
         email: this.email,
@@ -122,7 +123,7 @@ export class CreateAccountModel {
       /* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
       track('Create Account')
     } catch (e: any) {
-      this.rootStore.onboarding.skip() // undo starting the onboard
+      onboardingDispatch({type: 'skip'}) // undo starting the onboard
       let errMsg = e.toString()
       if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
         errMsg =
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index c00ee500a..708930610 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -7,9 +7,9 @@ const accountSchema = z.object({
   did: z.string(),
   refreshJwt: z.string().optional(),
   accessJwt: z.string().optional(),
-  handle: z.string(),
-  displayName: z.string(),
-  aviUrl: z.string(),
+  handle: z.string().optional(),
+  displayName: z.string().optional(),
+  aviUrl: z.string().optional(),
 })
 
 export const schema = z.object({
diff --git a/src/state/shell/color-mode.tsx b/src/state/shell/color-mode.tsx
index 74379da37..c6a4b8a18 100644
--- a/src/state/shell/color-mode.tsx
+++ b/src/state/shell/color-mode.tsx
@@ -27,7 +27,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       setState(persisted.get('colorMode'))
       updateDocument(persisted.get('colorMode'))
     })
-  }, [setStateWrapped])
+  }, [setState])
 
   return (
     <stateContext.Provider value={state}>
diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx
index 807ee79ab..0bb8988a6 100644
--- a/src/state/shell/index.tsx
+++ b/src/state/shell/index.tsx
@@ -4,6 +4,7 @@ import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled'
 import {Provider as MinimalModeProvider} from './minimal-mode'
 import {Provider as ColorModeProvider} from './color-mode'
 import {Provider as AltTextRequiredProvider} from './alt-text-required'
+import {Provider as OnboardingProvider} from './onboarding'
 
 export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
 export {
@@ -16,6 +17,7 @@ export {
   useRequireAltTextEnabled,
   useSetRequireAltTextEnabled,
 } from './alt-text-required'
+export {useOnboardingState, useOnboardingDispatch} from './onboarding'
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   return (
@@ -23,7 +25,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       <DrawerSwipableProvider>
         <MinimalModeProvider>
           <ColorModeProvider>
-            <AltTextRequiredProvider>{children}</AltTextRequiredProvider>
+            <OnboardingProvider>
+              <AltTextRequiredProvider>{children}</AltTextRequiredProvider>
+            </OnboardingProvider>
           </ColorModeProvider>
         </MinimalModeProvider>
       </DrawerSwipableProvider>
diff --git a/src/state/shell/onboarding.tsx b/src/state/shell/onboarding.tsx
new file mode 100644
index 000000000..5963cc50e
--- /dev/null
+++ b/src/state/shell/onboarding.tsx
@@ -0,0 +1,119 @@
+import React from 'react'
+import * as persisted from '#/state/persisted'
+import {track} from '#/lib/analytics/analytics'
+
+export const OnboardingScreenSteps = {
+  Welcome: 'Welcome',
+  RecommendedFeeds: 'RecommendedFeeds',
+  RecommendedFollows: 'RecommendedFollows',
+  Home: 'Home',
+} as const
+
+type OnboardingStep =
+  (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
+const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
+
+type Action =
+  | {type: 'set'; step: OnboardingStep}
+  | {type: 'next'; currentStep?: OnboardingStep}
+  | {type: 'start'}
+  | {type: 'finish'}
+  | {type: 'skip'}
+
+export type StateContext = persisted.Schema['onboarding'] & {
+  isComplete: boolean
+  isActive: boolean
+}
+export type DispatchContext = (action: Action) => void
+
+const stateContext = React.createContext<StateContext>(
+  compute(persisted.defaults.onboarding),
+)
+const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
+
+function reducer(state: StateContext, action: Action): StateContext {
+  switch (action.type) {
+    case 'set': {
+      if (OnboardingStepsArray.includes(action.step)) {
+        persisted.write('onboarding', {step: action.step})
+        return compute({...state, step: action.step})
+      }
+      return state
+    }
+    case 'next': {
+      const currentStep = action.currentStep || state.step
+      let nextStep = 'Home'
+      if (currentStep === 'Welcome') {
+        nextStep = 'RecommendedFeeds'
+      } else if (currentStep === 'RecommendedFeeds') {
+        nextStep = 'RecommendedFollows'
+      } else if (currentStep === 'RecommendedFollows') {
+        nextStep = 'Home'
+      }
+      persisted.write('onboarding', {step: nextStep})
+      return compute({...state, step: nextStep})
+    }
+    case 'start': {
+      track('Onboarding:Begin')
+      persisted.write('onboarding', {step: 'Welcome'})
+      return compute({...state, step: 'Welcome'})
+    }
+    case 'finish': {
+      track('Onboarding:Complete')
+      persisted.write('onboarding', {step: 'Home'})
+      return compute({...state, step: 'Home'})
+    }
+    case 'skip': {
+      track('Onboarding:Skipped')
+      persisted.write('onboarding', {step: 'Home'})
+      return compute({...state, step: 'Home'})
+    }
+    default: {
+      throw new Error('Invalid action')
+    }
+  }
+}
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [state, dispatch] = React.useReducer(
+    reducer,
+    compute(persisted.get('onboarding')),
+  )
+
+  React.useEffect(() => {
+    return persisted.onUpdate(() => {
+      dispatch({
+        type: 'set',
+        step: persisted.get('onboarding').step as OnboardingStep,
+      })
+    })
+  }, [dispatch])
+
+  return (
+    <stateContext.Provider value={state}>
+      <dispatchContext.Provider value={dispatch}>
+        {children}
+      </dispatchContext.Provider>
+    </stateContext.Provider>
+  )
+}
+
+export function useOnboardingState() {
+  return React.useContext(stateContext)
+}
+
+export function useOnboardingDispatch() {
+  return React.useContext(dispatchContext)
+}
+
+export function isOnboardingActive() {
+  return compute(persisted.get('onboarding')).isActive
+}
+
+function compute(state: persisted.Schema['onboarding']): StateContext {
+  return {
+    ...state,
+    isActive: state.step !== 'Home',
+    isComplete: state.step === 'Home',
+  }
+}
diff --git a/src/view/com/auth/Onboarding.tsx b/src/view/com/auth/Onboarding.tsx
index bec1dc236..994f4c149 100644
--- a/src/view/com/auth/Onboarding.tsx
+++ b/src/view/com/auth/Onboarding.tsx
@@ -4,34 +4,35 @@ import {observer} from 'mobx-react-lite'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {Welcome} from './onboarding/Welcome'
 import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
 import {RecommendedFollows} from './onboarding/RecommendedFollows'
 import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
+import {useOnboardingState, useOnboardingDispatch} from '#/state/shell'
 
 export const Onboarding = observer(function OnboardingImpl() {
   const pal = usePalette('default')
-  const store = useStores()
   const setMinimalShellMode = useSetMinimalShellMode()
+  const onboardingState = useOnboardingState()
+  const onboardingDispatch = useOnboardingDispatch()
 
   React.useEffect(() => {
     setMinimalShellMode(true)
   }, [setMinimalShellMode])
 
-  const next = () => store.onboarding.next()
-  const skip = () => store.onboarding.skip()
+  const next = () => onboardingDispatch({type: 'next'})
+  const skip = () => onboardingDispatch({type: 'skip'})
 
   return (
     <SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
       <ErrorBoundary>
-        {store.onboarding.step === 'Welcome' && (
+        {onboardingState.step === 'Welcome' && (
           <Welcome skip={skip} next={next} />
         )}
-        {store.onboarding.step === 'RecommendedFeeds' && (
+        {onboardingState.step === 'RecommendedFeeds' && (
           <RecommendedFeeds next={next} />
         )}
-        {store.onboarding.step === 'RecommendedFollows' && (
+        {onboardingState.step === 'RecommendedFollows' && (
           <RecommendedFollows next={next} />
         )}
       </ErrorBoundary>
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 1d64cc067..c3cfb3ad3 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -15,6 +15,7 @@ import {s} from 'lib/styles'
 import {useStores} from 'state/index'
 import {CreateAccountModel} from 'state/models/ui/create-account'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useOnboardingDispatch} from '#/state/shell'
 
 import {Step1} from './Step1'
 import {Step2} from './Step2'
@@ -29,6 +30,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
   const pal = usePalette('default')
   const store = useStores()
   const model = React.useMemo(() => new CreateAccountModel(store), [store])
+  const onboardingDispatch = useOnboardingDispatch()
 
   React.useEffect(() => {
     screen('CreateAccount')
@@ -59,14 +61,14 @@ export const CreateAccount = observer(function CreateAccountImpl({
       model.next()
     } else {
       try {
-        await model.submit()
+        await model.submit(onboardingDispatch)
       } catch {
         // dont need to handle here
       } finally {
         track('Try Create Account')
       }
     }
-  }, [model, track])
+  }, [model, track, onboardingDispatch])
 
   return (
     <LoggedOutLayout
diff --git a/src/view/com/auth/onboarding/RecommendedFollows.tsx b/src/view/com/auth/onboarding/RecommendedFollows.tsx
index f2710d2ac..9eef14e0b 100644
--- a/src/view/com/auth/onboarding/RecommendedFollows.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollows.tsx
@@ -11,6 +11,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {RecommendedFollowsItem} from './RecommendedFollowsItem'
+import {SuggestedActorsModel} from '#/state/models/discovery/suggested-actors'
 
 type Props = {
   next: () => void
@@ -21,16 +22,10 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
   const store = useStores()
   const pal = usePalette('default')
   const {isTabletOrMobile} = useWebMediaQueries()
-
-  React.useEffect(() => {
-    // Load suggested actors if not already loaded
-    // prefetch should happen in the onboarding model
-    if (
-      !store.onboarding.suggestedActors.hasLoaded ||
-      store.onboarding.suggestedActors.isEmpty
-    ) {
-      store.onboarding.suggestedActors.loadMore(true)
-    }
+  const suggestedActors = React.useMemo(() => {
+    const model = new SuggestedActorsModel(store)
+    model.refresh()
+    return model
   }, [store])
 
   const title = (
@@ -98,13 +93,19 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
           horizontal
           titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
           contentStyle={{paddingHorizontal: 0}}>
-          {store.onboarding.suggestedActors.isLoading ? (
+          {suggestedActors.isLoading ? (
             <ActivityIndicator size="large" />
           ) : (
             <FlatList
-              data={store.onboarding.suggestedActors.suggestions}
+              data={suggestedActors.suggestions}
               renderItem={({item, index}) => (
-                <RecommendedFollowsItem item={item} index={index} />
+                <RecommendedFollowsItem
+                  item={item}
+                  index={index}
+                  insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind(
+                    suggestedActors,
+                  )}
+                />
               )}
               keyExtractor={(item, index) => item.did + index.toString()}
               style={{flex: 1}}
@@ -126,13 +127,19 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
               users.
             </Text>
           </View>
-          {store.onboarding.suggestedActors.isLoading ? (
+          {suggestedActors.isLoading ? (
             <ActivityIndicator size="large" />
           ) : (
             <FlatList
-              data={store.onboarding.suggestedActors.suggestions}
+              data={suggestedActors.suggestions}
               renderItem={({item, index}) => (
-                <RecommendedFollowsItem item={item} index={index} />
+                <RecommendedFollowsItem
+                  item={item}
+                  index={index}
+                  insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind(
+                    suggestedActors,
+                  )}
+                />
               )}
               keyExtractor={(item, index) => item.did + index.toString()}
               style={{flex: 1}}
diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 2b26918d0..f672372b8 100644
--- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo} from 'react'
+import React from 'react'
 import {View, StyleSheet, ActivityIndicator} from 'react-native'
 import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
 import {observer} from 'mobx-react-lite'
@@ -18,22 +18,19 @@ import {useAnalytics} from 'lib/analytics/analytics'
 type Props = {
   item: SuggestedActor
   index: number
+  insertSuggestionsByActor: (did: string, index: number) => Promise<void>
 }
-export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
+export const RecommendedFollowsItem: React.FC<Props> = ({
+  item,
+  index,
+  insertSuggestionsByActor,
+}) => {
   const pal = usePalette('default')
-  const store = useStores()
   const {isMobile} = useWebMediaQueries()
-  const delay = useMemo(() => {
-    return (
-      50 *
-      (Math.abs(store.onboarding.suggestedActors.lastInsertedAtIndex - index) %
-        5)
-    )
-  }, [index, store.onboarding.suggestedActors.lastInsertedAtIndex])
 
   return (
     <Animated.View
-      entering={FadeInRight.delay(delay).springify()}
+      entering={FadeInRight}
       style={[
         styles.cardContainer,
         pal.view,
@@ -43,7 +40,12 @@ export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
           borderRightWidth: isMobile ? undefined : 1,
         },
       ]}>
-      <ProfileCard key={item.did} profile={item} index={index} />
+      <ProfileCard
+        key={item.did}
+        profile={item}
+        index={index}
+        insertSuggestionsByActor={insertSuggestionsByActor}
+      />
     </Animated.View>
   )
 }
@@ -51,9 +53,11 @@ export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
 export const ProfileCard = observer(function ProfileCardImpl({
   profile,
   index,
+  insertSuggestionsByActor,
 }: {
   profile: AppBskyActorDefs.ProfileViewBasic
   index: number
+  insertSuggestionsByActor: (did: string, index: number) => Promise<void>
 }) {
   const {track} = useAnalytics()
   const store = useStores()
@@ -94,10 +98,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
           onToggleFollow={async isFollow => {
             if (isFollow) {
               setAddingMoreSuggestions(true)
-              await store.onboarding.suggestedActors.insertSuggestionsByActor(
-                profile.did,
-                index,
-              )
+              await insertSuggestionsByActor(profile.did, index)
               setAddingMoreSuggestions(false)
               track('Onboarding:SuggestedFollowFollowed')
             }
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
index 25d12165f..898f81051 100644
--- a/src/view/com/auth/withAuthRequired.tsx
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -13,19 +13,21 @@ import {Onboarding} from './Onboarding'
 import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {STATUS_PAGE_URL} from 'lib/constants'
+import {useOnboardingState} from '#/state/shell'
 
 export const withAuthRequired = <P extends object>(
   Component: React.ComponentType<P>,
 ): React.FC<P> =>
   observer(function AuthRequired(props: P) {
     const store = useStores()
+    const onboardingState = useOnboardingState()
     if (store.session.isResumingSession) {
       return <Loading />
     }
     if (!store.session.hasSession) {
       return <LoggedOut />
     }
-    if (store.onboarding.isActive) {
+    if (onboardingState.isActive) {
       return <Onboarding />
     }
     return <Component {...props} />
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 09a99e6d2..37c2961b4 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -52,6 +52,7 @@ import {
   useSetColorMode,
   useRequireAltTextEnabled,
   useSetRequireAltTextEnabled,
+  useOnboardingDispatch,
 } from '#/state/shell'
 
 // TEMPORARY (APP-700)
@@ -70,6 +71,7 @@ export const SettingsScreen = withAuthRequired(
     const setMinimalShellMode = useSetMinimalShellMode()
     const requireAltTextEnabled = useRequireAltTextEnabled()
     const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
+    const onboardingDispatch = useOnboardingDispatch()
     const navigation = useNavigation<NavigationProp>()
     const {isMobile} = useWebMediaQueries()
     const {screen, track} = useAnalytics()
@@ -157,9 +159,9 @@ export const SettingsScreen = withAuthRequired(
     }, [store])
 
     const onPressResetOnboarding = React.useCallback(async () => {
-      store.onboarding.reset()
+      onboardingDispatch({type: 'start'})
       Toast.show('Onboarding reset')
-    }, [store])
+    }, [onboardingDispatch])
 
     const onPressBuildInfo = React.useCallback(() => {
       Clipboard.setString(
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 843d0b284..1731ea247 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -17,12 +17,17 @@ import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useAuxClick} from 'lib/hooks/useAuxClick'
-import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
+import {
+  useIsDrawerOpen,
+  useSetDrawerOpen,
+  useOnboardingState,
+} from '#/state/shell'
 
 const ShellInner = observer(function ShellInnerImpl() {
   const store = useStores()
   const isDrawerOpen = useIsDrawerOpen()
   const setDrawerOpen = useSetDrawerOpen()
+  const onboardingState = useOnboardingState()
   const {isDesktop, isMobile} = useWebMediaQueries()
   const navigator = useNavigation<NavigationProp>()
   useAuxClick()
@@ -34,9 +39,9 @@ const ShellInner = observer(function ShellInnerImpl() {
     })
   }, [navigator, store.shell, setDrawerOpen])
 
-  const showBottomBar = isMobile && !store.onboarding.isActive
+  const showBottomBar = isMobile && !onboardingState.isActive
   const showSideNavs =
-    !isMobile && store.session.hasSession && !store.onboarding.isActive
+    !isMobile && store.session.hasSession && !onboardingState.isActive
   return (
     <View style={[s.hContentRegion, {overflow: 'hidden'}]}>
       <View style={s.hContentRegion}>