forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}