about summary refs log tree commit diff
path: root/src/state/shell/onboarding.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/shell/onboarding.tsx')
-rw-r--r--src/state/shell/onboarding.tsx123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/state/shell/onboarding.tsx b/src/state/shell/onboarding.tsx
new file mode 100644
index 000000000..6a18b461f
--- /dev/null
+++ b/src/state/shell/onboarding.tsx
@@ -0,0 +1,123 @@
+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(() => {
+      const next = persisted.get('onboarding').step
+      // TODO we've introduced a footgun
+      if (state.step !== next) {
+        dispatch({
+          type: 'set',
+          step: persisted.get('onboarding').step as OnboardingStep,
+        })
+      }
+    })
+  }, [state, 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',
+  }
+}