forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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)
32stateContext.displayName = 'OnboardingStateContext'
33const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
34dispatchContext.displayName = 'OnboardingDispatchContext'
35
36function reducer(state: StateContext, action: Action): StateContext {
37 switch (action.type) {
38 case 'set': {
39 if (OnboardingStepsArray.includes(action.step)) {
40 persisted.write('onboarding', {step: action.step})
41 return compute({...state, step: action.step})
42 }
43 return state
44 }
45 case 'next': {
46 const currentStep = action.currentStep || state.step
47 let nextStep = 'Home'
48 if (currentStep === 'Welcome') {
49 nextStep = 'RecommendedFeeds'
50 } else if (currentStep === 'RecommendedFeeds') {
51 nextStep = 'RecommendedFollows'
52 } else if (currentStep === 'RecommendedFollows') {
53 nextStep = 'Home'
54 }
55 persisted.write('onboarding', {step: nextStep})
56 return compute({...state, step: nextStep})
57 }
58 case 'start': {
59 persisted.write('onboarding', {step: 'Welcome'})
60 return compute({...state, step: 'Welcome'})
61 }
62 case 'finish': {
63 persisted.write('onboarding', {step: 'Home'})
64 return compute({...state, step: 'Home'})
65 }
66 case 'skip': {
67 persisted.write('onboarding', {step: 'Home'})
68 return compute({...state, step: 'Home'})
69 }
70 default: {
71 throw new Error('Invalid action')
72 }
73 }
74}
75
76export function Provider({children}: React.PropsWithChildren<{}>) {
77 const [state, dispatch] = React.useReducer(
78 reducer,
79 compute(persisted.get('onboarding')),
80 )
81
82 React.useEffect(() => {
83 return persisted.onUpdate('onboarding', nextOnboarding => {
84 const next = nextOnboarding.step
85 // TODO we've introduced a footgun
86 if (state.step !== next) {
87 dispatch({
88 type: 'set',
89 step: nextOnboarding.step as OnboardingStep,
90 })
91 }
92 })
93 }, [state, dispatch])
94
95 return (
96 <stateContext.Provider value={state}>
97 <dispatchContext.Provider value={dispatch}>
98 {children}
99 </dispatchContext.Provider>
100 </stateContext.Provider>
101 )
102}
103
104export function useOnboardingState() {
105 return React.useContext(stateContext)
106}
107
108export function useOnboardingDispatch() {
109 return React.useContext(dispatchContext)
110}
111
112export function isOnboardingActive() {
113 return compute(persisted.get('onboarding')).isActive
114}
115
116function compute(state: persisted.Schema['onboarding']): StateContext {
117 return {
118 ...state,
119 isActive: state.step !== 'Home',
120 isComplete: state.step === 'Home',
121 }
122}