add new gated screen to onboarding

This commit is contained in:
Samuel Newman
2025-08-14 16:53:09 +03:00
parent e91a683810
commit ac713bac08
5 changed files with 207 additions and 18 deletions
+1
View File
@@ -7,6 +7,7 @@ export type Gate =
| 'explore_show_suggested_feeds' | 'explore_show_suggested_feeds'
| 'old_postonboarding' | 'old_postonboarding'
| 'onboarding_add_video_feed' | 'onboarding_add_video_feed'
| 'onboarding_suggested_accounts'
| 'post_follow_profile_suggested_accounts' | 'post_follow_profile_suggested_accounts'
| 'post_threads_v2_unspecced' | 'post_threads_v2_unspecced'
| 'remove_show_latest_button' | 'remove_show_latest_button'
+1 -1
View File
@@ -11,7 +11,7 @@ import {
atoms as a, atoms as a,
flatten, flatten,
native, native,
TextStyleProp, type TextStyleProp,
useBreakpoints, useBreakpoints,
useTheme, useTheme,
web, web,
@@ -0,0 +1,146 @@
import {useCallback, useContext, useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useOnboardingDispatch} from '#/state/shell'
import {
DescriptionText,
OnboardingControls,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {IconCircle} from '#/components/IconCircle'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwise} from '#/components/icons/ArrowRotateCounterClockwise'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSad} from '#/components/icons/Emoji'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {Loader} from '#/components/Loader'
export function StepSuggestedAccounts() {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {dispatch} = useContext(Context)
const [saving, setSaving] = useState(false)
const onboardDispatch = useOnboardingDispatch()
const saveInterests = useCallback(async () => {
setSaving(true)
try {
setSaving(false)
dispatch({type: 'next'})
} catch (e: any) {
logger.info(`onboading: error saving interests`)
logger.error(e)
}
}, [setSaving, dispatch])
const skipOnboarding = useCallback(() => {
onboardDispatch({type: 'finish'})
dispatch({type: 'finish'})
}, [onboardDispatch, dispatch])
const isError = false
const title = isError ? (
<Trans>Oh no! Something went wrong.</Trans>
) : (
<Trans>Suggested Accounts</Trans>
)
const description = isError ? (
<Trans>
We weren't able to connect. Please try again to continue setting up your
account. If it continues to fail, you can skip this flow.
</Trans>
) : (
<Trans>We'll use this to help customize your experience.</Trans>
)
return (
<View style={[a.align_start]} testID="onboardingInterests">
<IconCircle
icon={isError ? EmojiSad : Hashtag}
style={[
a.mb_2xl,
isError
? {
backgroundColor: t.palette.negative_50,
}
: {},
]}
iconStyle={[
isError
? {
color: t.palette.negative_900,
}
: {},
]}
/>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
<View style={[a.w_full, a.pt_2xl]} />
<OnboardingControls.Portal>
{isError ? (
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
<Button
// disabled={isFetching}
variant="solid"
color="secondary"
size="large"
label={_(msg`Retry`)}
// onPress={() => refetch()}
>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
<ButtonIcon icon={ArrowRotateCounterClockwise} position="right" />
</Button>
<Button
variant="outline"
color="secondary"
size="large"
label={_(msg`Skip this flow`)}
onPress={skipOnboarding}>
<ButtonText>
<Trans>Skip</Trans>
</ButtonText>
</Button>
</View>
) : (
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
<Button
disabled={saving}
color="secondary"
size="large"
label={_(msg`Follow all accounts`)}>
<ButtonText>
<Trans>Follow all</Trans>
</ButtonText>
</Button>
<Button
disabled={saving}
variant="solid"
color="primary"
size="large"
label={_(msg`Continue to next step`)}
onPress={saveInterests}>
<ButtonText>
<Trans>Continue</Trans>
</ButtonText>
<ButtonIcon icon={saving ? Loader : ChevronRight} />
</Button>
</View>
)}
</OnboardingControls.Portal>
</View>
)
}
+15 -4
View File
@@ -1,21 +1,29 @@
import React from 'react' import {useMemo, useReducer} from 'react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout' import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
import {Context, initialState, reducer} from '#/screens/Onboarding/state' import {Context, initialState, reducer} from '#/screens/Onboarding/state'
import {StepFinished} from '#/screens/Onboarding/StepFinished' import {StepFinished} from '#/screens/Onboarding/StepFinished'
import {StepInterests} from '#/screens/Onboarding/StepInterests' import {StepInterests} from '#/screens/Onboarding/StepInterests'
import {StepProfile} from '#/screens/Onboarding/StepProfile' import {StepProfile} from '#/screens/Onboarding/StepProfile'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {StepSuggestedAccounts} from './StepSuggestedAccounts'
export function Onboarding() { export function Onboarding() {
const {_} = useLingui() const {_} = useLingui()
const [state, dispatch] = React.useReducer(reducer, { const gate = useGate()
const showSuggestedAccounts = gate('onboarding_suggested_accounts')
const [state, dispatch] = useReducer(reducer, {
...initialState, ...initialState,
totalSteps: showSuggestedAccounts ? 4 : 3,
experiments: {
onboarding_suggested_accounts: showSuggestedAccounts,
},
}) })
const interestsDisplayNames = React.useMemo(() => { const interestsDisplayNames = useMemo(() => {
return { return {
news: _(msg`News`), news: _(msg`News`),
journalism: _(msg`Journalism`), journalism: _(msg`Journalism`),
@@ -46,13 +54,16 @@ export function Onboarding() {
<Portal> <Portal>
<OnboardingControls.Provider> <OnboardingControls.Provider>
<Context.Provider <Context.Provider
value={React.useMemo( value={useMemo(
() => ({state, dispatch, interestsDisplayNames}), () => ({state, dispatch, interestsDisplayNames}),
[state, dispatch, interestsDisplayNames], [state, dispatch, interestsDisplayNames],
)}> )}>
<Layout> <Layout>
{state.activeStep === 'profile' && <StepProfile />} {state.activeStep === 'profile' && <StepProfile />}
{state.activeStep === 'interests' && <StepInterests />} {state.activeStep === 'interests' && <StepInterests />}
{state.activeStep === 'suggested-accounts' && (
<StepSuggestedAccounts />
)}
{state.activeStep === 'finished' && <StepFinished />} {state.activeStep === 'finished' && <StepFinished />}
</Layout> </Layout>
</Context.Provider> </Context.Provider>
+32 -1
View File
@@ -11,7 +11,7 @@ import {
export type OnboardingState = { export type OnboardingState = {
hasPrev: boolean hasPrev: boolean
totalSteps: number totalSteps: number
activeStep: 'profile' | 'interests' | 'finished' activeStep: 'profile' | 'interests' | 'suggested-accounts' | 'finished'
activeStepIndex: number activeStepIndex: number
interestsStepResults: { interestsStepResults: {
@@ -34,6 +34,10 @@ export type OnboardingState = {
backgroundColor: AvatarColor backgroundColor: AvatarColor
} }
} }
experiments?: {
onboarding_suggested_accounts?: boolean
}
} }
export type OnboardingAction = export type OnboardingAction =
@@ -160,6 +164,19 @@ export function reducer(
switch (a.type) { switch (a.type) {
case 'next': { case 'next': {
if (s.experiments?.onboarding_suggested_accounts) {
if (s.activeStep === 'profile') {
next.activeStep = 'interests'
next.activeStepIndex = 2
} else if (s.activeStep === 'interests') {
next.activeStep = 'suggested-accounts'
next.activeStepIndex = 3
}
if (s.activeStep === 'suggested-accounts') {
next.activeStep = 'finished'
next.activeStepIndex = 4
}
} else {
if (s.activeStep === 'profile') { if (s.activeStep === 'profile') {
next.activeStep = 'interests' next.activeStep = 'interests'
next.activeStepIndex = 2 next.activeStepIndex = 2
@@ -167,9 +184,22 @@ export function reducer(
next.activeStep = 'finished' next.activeStep = 'finished'
next.activeStepIndex = 3 next.activeStepIndex = 3
} }
}
break break
} }
case 'prev': { case 'prev': {
if (s.experiments?.onboarding_suggested_accounts) {
if (s.activeStep === 'interests') {
next.activeStep = 'profile'
next.activeStepIndex = 1
} else if (s.activeStep === 'suggested-accounts') {
next.activeStep = 'interests'
next.activeStepIndex = 2
} else if (s.activeStep === 'finished') {
next.activeStep = 'suggested-accounts'
next.activeStepIndex = 3
}
} else {
if (s.activeStep === 'interests') { if (s.activeStep === 'interests') {
next.activeStep = 'profile' next.activeStep = 'profile'
next.activeStepIndex = 1 next.activeStepIndex = 1
@@ -177,6 +207,7 @@ export function reducer(
next.activeStep = 'interests' next.activeStep = 'interests'
next.activeStepIndex = 2 next.activeStepIndex = 2
} }
}
break break
} }
case 'finish': { case 'finish': {