Files
bsky-social-app/src/screens/Onboarding/state.ts
T
Samuel Newman 1127e50248 [Onboarding] Add starterpacks step (#9007)
* add onboarding starterpacks step

* tweak starter pack cards

* fix web styling

* use new API

* add screen transitions to onboarding

* skip host account

* bump limit to 6

* tweak font weight

* rm commented out code

* fix yarn.lock

* limit starter packs experiment to english speakers
2025-10-15 07:32:38 -07:00

252 lines
5.5 KiB
TypeScript

import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {
type AvatarColor,
type Emoji,
} from '#/screens/Onboarding/StepProfile/types'
export type OnboardingState = {
hasPrev: boolean
totalSteps: number
activeStep:
| 'profile'
| 'interests'
| 'suggested-accounts'
| 'suggested-starterpacks'
| 'finished'
activeStepIndex: number
stepTransitionDirection: 'Forward' | 'Backward'
interestsStepResults: {
selectedInterests: string[]
apiResponse: ApiResponseMap
}
profileStepResults: {
isCreatedAvatar: boolean
image?: {
path: string
mime: string
size: number
width: number
height: number
}
imageUri?: string
imageMime?: string
creatorState?: {
emoji: Emoji
backgroundColor: AvatarColor
}
}
experiments?: {
onboarding_suggested_accounts?: boolean
onboarding_value_prop?: boolean
onboarding_suggested_starterpacks?: boolean
}
}
export type OnboardingAction =
| {
type: 'next'
}
| {
type: 'prev'
}
| {
type: 'finish'
}
| {
type: 'setInterestsStepResults'
selectedInterests: string[]
apiResponse: ApiResponseMap
}
| {
type: 'setProfileStepResults'
isCreatedAvatar: boolean
image: OnboardingState['profileStepResults']['image'] | undefined
imageUri: string | undefined
imageMime: string
creatorState:
| {
emoji: Emoji
backgroundColor: AvatarColor
}
| undefined
}
export type ApiResponseMap = {
interests: string[]
suggestedAccountDids: {
[key: string]: string[]
}
suggestedFeedUris: {
[key: string]: string[]
}
}
// most popular selected interests
export const popularInterests = [
'art',
'gaming',
'sports',
'comics',
'music',
'politics',
'photography',
'science',
'news',
]
export function useInterestsDisplayNames() {
const {_} = useLingui()
return React.useMemo<Record<string, string>>(() => {
return {
// Keep this alphabetized
animals: _(msg`Animals`),
art: _(msg`Art`),
books: _(msg`Books`),
comedy: _(msg`Comedy`),
comics: _(msg`Comics`),
culture: _(msg`Culture`),
dev: _(msg`Software Dev`),
education: _(msg`Education`),
food: _(msg`Food`),
gaming: _(msg`Video Games`),
journalism: _(msg`Journalism`),
movies: _(msg`Movies`),
music: _(msg`Music`),
nature: _(msg`Nature`),
news: _(msg`News`),
pets: _(msg`Pets`),
photography: _(msg`Photography`),
politics: _(msg`Politics`),
science: _(msg`Science`),
sports: _(msg`Sports`),
tech: _(msg`Tech`),
tv: _(msg`TV`),
writers: _(msg`Writers`),
}
}, [_])
}
export const initialState: OnboardingState = {
hasPrev: false,
totalSteps: 3,
activeStep: 'profile',
activeStepIndex: 1,
stepTransitionDirection: 'Forward',
interestsStepResults: {
selectedInterests: [],
apiResponse: {
interests: [],
suggestedAccountDids: {},
suggestedFeedUris: {},
},
},
profileStepResults: {
isCreatedAvatar: false,
image: undefined,
imageUri: '',
imageMime: '',
},
}
export const Context = React.createContext<{
state: OnboardingState
dispatch: React.Dispatch<OnboardingAction>
}>({
state: {...initialState},
dispatch: () => {},
})
Context.displayName = 'OnboardingContext'
export function reducer(
s: OnboardingState,
a: OnboardingAction,
): OnboardingState {
let next = {...s}
const stepOrder: OnboardingState['activeStep'][] = [
'profile',
'interests',
...(s.experiments?.onboarding_suggested_accounts
? (['suggested-accounts'] as const)
: []),
...(s.experiments?.onboarding_suggested_starterpacks
? (['suggested-starterpacks'] as const)
: []),
'finished',
]
switch (a.type) {
case 'next': {
// 1-indexed for some reason
const nextIndex = s.activeStepIndex
const nextStep = stepOrder[nextIndex]
if (nextStep) {
next.activeStep = nextStep
next.activeStepIndex = nextIndex + 1
}
next.stepTransitionDirection = 'Forward'
break
}
case 'prev': {
const prevIndex = s.activeStepIndex - 2
const prevStep = stepOrder[prevIndex]
if (prevStep) {
next.activeStep = prevStep
next.activeStepIndex = prevIndex + 1
}
next.stepTransitionDirection = 'Backward'
break
}
case 'finish': {
next = initialState
break
}
case 'setInterestsStepResults': {
next.interestsStepResults = {
selectedInterests: a.selectedInterests,
apiResponse: a.apiResponse,
}
break
}
case 'setProfileStepResults': {
next.profileStepResults = {
isCreatedAvatar: a.isCreatedAvatar,
image: a.image,
imageUri: a.imageUri,
imageMime: a.imageMime,
creatorState: a.creatorState,
}
break
}
}
const state = {
...next,
hasPrev: next.activeStep !== 'profile',
}
logger.debug(`onboarding`, {
hasPrev: state.hasPrev,
activeStep: state.activeStep,
activeStepIndex: state.activeStepIndex,
interestsStepResults: {
selectedInterests: state.interestsStepResults.selectedInterests,
},
profileStepResults: state.profileStepResults,
})
if (s.activeStep !== state.activeStep) {
logger.debug(`onboarding: step changed`, {activeStep: state.activeStep})
}
return state
}