about summary refs log tree commit diff
path: root/src/state/shell/onboarding.tsx
blob: 6b48f72ad6a185dcff6d49b0b3bcffdd66ec7d8c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import React from 'react'

import * as persisted from '#/state/persisted'

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),
)
stateContext.displayName = 'OnboardingStateContext'
const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
dispatchContext.displayName = 'OnboardingDispatchContext'

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': {
      persisted.write('onboarding', {step: 'Welcome'})
      return compute({...state, step: 'Welcome'})
    }
    case 'finish': {
      persisted.write('onboarding', {step: 'Home'})
      return compute({...state, step: 'Home'})
    }
    case 'skip': {
      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('onboarding', nextOnboarding => {
      const next = nextOnboarding.step
      // TODO we've introduced a footgun
      if (state.step !== next) {
        dispatch({
          type: 'set',
          step: nextOnboarding.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',
  }
}