Bluesky app fork with some witchin' additions 馃挮
at main 248 lines 6.3 kB view raw
1import {createContext, useContext, useMemo} from 'react' 2 3import {logger} from '#/logger' 4import { 5 type AvatarColor, 6 type Emoji, 7} from '#/screens/Onboarding/StepProfile/types' 8 9type OnboardingScreen = 10 | 'profile' 11 | 'interests' 12 | 'suggested-accounts' 13 | 'suggested-starterpacks' 14 | 'find-contacts-intro' 15 | 'find-contacts' 16 | 'finished' 17 18export type OnboardingState = { 19 screens: Record<OnboardingScreen, boolean> 20 activeStep: OnboardingScreen 21 stepTransitionDirection: 'Forward' | 'Backward' 22 23 interestsStepResults: { 24 selectedInterests: string[] 25 } 26 profileStepResults: { 27 isCreatedAvatar: boolean 28 image?: { 29 path: string 30 mime: string 31 size: number 32 width: number 33 height: number 34 } 35 imageUri?: string 36 imageMime?: string 37 creatorState?: { 38 emoji: Emoji 39 backgroundColor: AvatarColor 40 } 41 } 42} 43 44export type OnboardingAction = 45 | { 46 type: 'next' 47 } 48 | { 49 type: 'prev' 50 } 51 | { 52 type: 'skip-contacts' 53 } 54 | { 55 type: 'finish' 56 } 57 | { 58 type: 'setInterestsStepResults' 59 selectedInterests: string[] 60 } 61 | { 62 type: 'setProfileStepResults' 63 isCreatedAvatar: boolean 64 image: OnboardingState['profileStepResults']['image'] | undefined 65 imageUri: string | undefined 66 imageMime: string 67 creatorState: 68 | { 69 emoji: Emoji 70 backgroundColor: AvatarColor 71 } 72 | undefined 73 } 74 75export function createInitialOnboardingState( 76 { 77 starterPacksStepEnabled, 78 findContactsStepEnabled, 79 }: { 80 starterPacksStepEnabled: boolean 81 findContactsStepEnabled: boolean 82 } = {starterPacksStepEnabled: true, findContactsStepEnabled: false}, 83): OnboardingState { 84 const screens: OnboardingState['screens'] = { 85 profile: true, 86 interests: true, 87 'suggested-accounts': true, 88 'suggested-starterpacks': starterPacksStepEnabled, 89 'find-contacts-intro': findContactsStepEnabled, 90 'find-contacts': findContactsStepEnabled, 91 finished: true, 92 } 93 94 return { 95 screens, 96 activeStep: 'profile', 97 stepTransitionDirection: 'Forward', 98 interestsStepResults: { 99 selectedInterests: [], 100 }, 101 profileStepResults: { 102 isCreatedAvatar: false, 103 image: undefined, 104 imageUri: '', 105 imageMime: '', 106 }, 107 } 108} 109 110export const Context = createContext<{ 111 state: OnboardingState 112 dispatch: React.Dispatch<OnboardingAction> 113} | null>(null) 114Context.displayName = 'OnboardingContext' 115 116export function reducer( 117 s: OnboardingState, 118 a: OnboardingAction, 119): OnboardingState { 120 let next = {...s} 121 122 const stepOrder = getStepOrder(s) 123 124 switch (a.type) { 125 case 'next': { 126 const nextIndex = stepOrder.indexOf(next.activeStep) + 1 127 const nextStep = stepOrder[nextIndex] 128 if (nextStep) { 129 next.activeStep = nextStep 130 } 131 next.stepTransitionDirection = 'Forward' 132 break 133 } 134 case 'prev': { 135 const prevIndex = stepOrder.indexOf(next.activeStep) - 1 136 const prevStep = stepOrder[prevIndex] 137 if (prevStep) { 138 next.activeStep = prevStep 139 } 140 next.stepTransitionDirection = 'Backward' 141 break 142 } 143 case 'skip-contacts': { 144 const nextIndex = stepOrder.indexOf('find-contacts') + 1 145 const nextStep = stepOrder[nextIndex] ?? 'finished' 146 next.activeStep = nextStep 147 next.stepTransitionDirection = 'Forward' 148 break 149 } 150 case 'finish': { 151 next = createInitialOnboardingState({ 152 starterPacksStepEnabled: s.screens['suggested-starterpacks'], 153 findContactsStepEnabled: s.screens['find-contacts'], 154 }) 155 break 156 } 157 case 'setInterestsStepResults': { 158 next.interestsStepResults = { 159 selectedInterests: a.selectedInterests, 160 } 161 break 162 } 163 case 'setProfileStepResults': { 164 next.profileStepResults = { 165 isCreatedAvatar: a.isCreatedAvatar, 166 image: a.image, 167 imageUri: a.imageUri, 168 imageMime: a.imageMime, 169 creatorState: a.creatorState, 170 } 171 break 172 } 173 } 174 175 const state = { 176 ...next, 177 hasPrev: next.activeStep !== 'profile', 178 } 179 180 logger.debug(`onboarding`, { 181 hasPrev: state.hasPrev, 182 activeStep: state.activeStep, 183 interestsStepResults: { 184 selectedInterests: state.interestsStepResults.selectedInterests, 185 }, 186 profileStepResults: state.profileStepResults, 187 }) 188 189 if (s.activeStep !== state.activeStep) { 190 logger.debug(`onboarding: step changed`, {activeStep: state.activeStep}) 191 } 192 193 return state 194} 195 196function getStepOrder(s: OnboardingState): OnboardingScreen[] { 197 return [ 198 s.screens.profile && ('profile' as const), 199 s.screens.interests && ('interests' as const), 200 s.screens['suggested-accounts'] && ('suggested-accounts' as const), 201 s.screens['suggested-starterpacks'] && ('suggested-starterpacks' as const), 202 s.screens['find-contacts-intro'] && ('find-contacts-intro' as const), 203 s.screens['find-contacts'] && ('find-contacts' as const), 204 s.screens.finished && ('finished' as const), 205 ].filter(x => !!x) 206} 207 208/** 209 * Note: not to be confused with `useOnboardingState`, which just determines if onboarding is active. 210 * This hook is for internal state of the onboarding flow (i.e. active step etc). 211 * 212 * This adds additional derived state to the onboarding context reducer. 213 */ 214export function useOnboardingInternalState() { 215 const ctx = useContext(Context) 216 217 if (!ctx) { 218 throw new Error( 219 'useOnboardingInternalState must be used within OnboardingContext', 220 ) 221 } 222 223 const {state, dispatch} = ctx 224 225 return { 226 state: useMemo(() => { 227 const stepOrder = getStepOrder(state).filter( 228 x => x !== 'find-contacts' && x !== 'finished', 229 ) as string[] 230 const canGoBack = state.activeStep !== stepOrder[0] 231 return { 232 ...state, 233 canGoBack, 234 /** 235 * Note: for *display* purposes only, do not lean on this 236 * for navigation purposes! we merge certain steps! 237 */ 238 activeStepIndex: stepOrder.indexOf( 239 state.activeStep === 'find-contacts' 240 ? 'find-contacts-intro' 241 : state.activeStep, 242 ), 243 totalSteps: stepOrder.length, 244 } 245 }, [state]), 246 dispatch, 247 } 248}