my fork of the bluesky client
at main 120 lines 3.3 kB view raw
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}