my fork of the bluesky client
1import React from 'react'
2
3import * as persisted from '#/state/persisted'
4
5export const OnboardingScreenSteps = {
6 Welcome: 'Welcome',
7 RecommendedFeeds: 'RecommendedFeeds',
8 RecommendedFollows: 'RecommendedFollows',
9 Home: 'Home',
10} as const
11
12type OnboardingStep =
13 (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
14const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
15
16type Action =
17 | {type: 'set'; step: OnboardingStep}
18 | {type: 'next'; currentStep?: OnboardingStep}
19 | {type: 'start'}
20 | {type: 'finish'}
21 | {type: 'skip'}
22
23export type StateContext = persisted.Schema['onboarding'] & {
24 isComplete: boolean
25 isActive: boolean
26}
27export type DispatchContext = (action: Action) => void
28
29const stateContext = React.createContext<StateContext>(
30 compute(persisted.defaults.onboarding),
31)
32const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
33
34function reducer(state: StateContext, action: Action): StateContext {
35 switch (action.type) {
36 case 'set': {
37 if (OnboardingStepsArray.includes(action.step)) {
38 persisted.write('onboarding', {step: action.step})
39 return compute({...state, step: action.step})
40 }
41 return state
42 }
43 case 'next': {
44 const currentStep = action.currentStep || state.step
45 let nextStep = 'Home'
46 if (currentStep === 'Welcome') {
47 nextStep = 'RecommendedFeeds'
48 } else if (currentStep === 'RecommendedFeeds') {
49 nextStep = 'RecommendedFollows'
50 } else if (currentStep === 'RecommendedFollows') {
51 nextStep = 'Home'
52 }
53 persisted.write('onboarding', {step: nextStep})
54 return compute({...state, step: nextStep})
55 }
56 case 'start': {
57 persisted.write('onboarding', {step: 'Welcome'})
58 return compute({...state, step: 'Welcome'})
59 }
60 case 'finish': {
61 persisted.write('onboarding', {step: 'Home'})
62 return compute({...state, step: 'Home'})
63 }
64 case 'skip': {
65 persisted.write('onboarding', {step: 'Home'})
66 return compute({...state, step: 'Home'})
67 }
68 default: {
69 throw new Error('Invalid action')
70 }
71 }
72}
73
74export function Provider({children}: React.PropsWithChildren<{}>) {
75 const [state, dispatch] = React.useReducer(
76 reducer,
77 compute(persisted.get('onboarding')),
78 )
79
80 React.useEffect(() => {
81 return persisted.onUpdate('onboarding', nextOnboarding => {
82 const next = nextOnboarding.step
83 // TODO we've introduced a footgun
84 if (state.step !== next) {
85 dispatch({
86 type: 'set',
87 step: nextOnboarding.step as OnboardingStep,
88 })
89 }
90 })
91 }, [state, dispatch])
92
93 return (
94 <stateContext.Provider value={state}>
95 <dispatchContext.Provider value={dispatch}>
96 {children}
97 </dispatchContext.Provider>
98 </stateContext.Provider>
99 )
100}
101
102export function useOnboardingState() {
103 return React.useContext(stateContext)
104}
105
106export function useOnboardingDispatch() {
107 return React.useContext(dispatchContext)
108}
109
110export function isOnboardingActive() {
111 return compute(persisted.get('onboarding')).isActive
112}
113
114function compute(state: persisted.Schema['onboarding']): StateContext {
115 return {
116 ...state,
117 isActive: state.step !== 'Home',
118 isComplete: state.step === 'Home',
119 }
120}