Add value proposition screen experiment (#8898)
* add assets * add value prop experiment * add alt text * add metrics * add transitions * add skip button * tweak copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add borderless variant for web * rm pointer events --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 103 KiB |
@@ -8,6 +8,7 @@ export type Gate =
|
|||||||
| 'old_postonboarding'
|
| 'old_postonboarding'
|
||||||
| 'onboarding_add_video_feed'
|
| 'onboarding_add_video_feed'
|
||||||
| 'onboarding_suggested_accounts'
|
| 'onboarding_suggested_accounts'
|
||||||
|
| 'onboarding_value_prop'
|
||||||
| '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'
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export type MetricEvents = {
|
|||||||
'onboarding:finished:avatarResult': {
|
'onboarding:finished:avatarResult': {
|
||||||
avatarResult: 'default' | 'created' | 'uploaded'
|
avatarResult: 'default' | 'created' | 'uploaded'
|
||||||
}
|
}
|
||||||
|
'onboarding:valueProp:stepOne:nextPressed': {}
|
||||||
|
'onboarding:valueProp:stepTwo:nextPressed': {}
|
||||||
|
'onboarding:valueProp:skipPressed': {}
|
||||||
'home:feedDisplayed': {
|
'home:feedDisplayed': {
|
||||||
feedUrl: string
|
feedUrl: string
|
||||||
feedType: string
|
feedType: string
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ import {
|
|||||||
import {leading} from '#/alf/typography'
|
import {leading} from '#/alf/typography'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||||
|
import {HEADER_SLOT_SIZE} from '#/components/Layout'
|
||||||
import {createPortalGroup} from '#/components/Portal'
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
import {P, Text} from '#/components/Typography'
|
import {P, Text} from '#/components/Typography'
|
||||||
|
|
||||||
const ONBOARDING_COL_WIDTH = 420
|
const ONBOARDING_COL_WIDTH = 420
|
||||||
|
|
||||||
export const OnboardingControls = createPortalGroup()
|
export const OnboardingControls = createPortalGroup()
|
||||||
|
export const OnboardingHeaderSlot = createPortalGroup()
|
||||||
|
|
||||||
export function Layout({children}: React.PropsWithChildren<{}>) {
|
export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -65,46 +67,67 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
]}>
|
]}>
|
||||||
{__DEV__ && (
|
{__DEV__ && (
|
||||||
<View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
color="negative"
|
||||||
color="negative"
|
size="tiny"
|
||||||
size="small"
|
onPress={() => onboardDispatch({type: 'skip'})}
|
||||||
onPress={() => onboardDispatch({type: 'skip'})}
|
// DEV ONLY
|
||||||
// DEV ONLY
|
label="Clear onboarding state"
|
||||||
label="Clear onboarding state">
|
style={[
|
||||||
<ButtonText>Clear</ButtonText>
|
a.absolute,
|
||||||
</Button>
|
a.z_10,
|
||||||
</View>
|
{
|
||||||
|
left: '50%',
|
||||||
|
top: insets.top + 2,
|
||||||
|
transform: [{translateX: '-50%'}],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<ButtonText>[DEV] Clear</ButtonText>
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!gtMobile && state.hasPrev && (
|
{!gtMobile && (
|
||||||
<View
|
<View
|
||||||
|
pointerEvents="box-none"
|
||||||
style={[
|
style={[
|
||||||
web(a.fixed),
|
web(a.fixed),
|
||||||
native(a.absolute),
|
native(a.absolute),
|
||||||
|
a.left_0,
|
||||||
|
a.right_0,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
a.z_20,
|
a.z_20,
|
||||||
a.px_xl,
|
a.px_xl,
|
||||||
{
|
{top: paddingTop.paddingTop + insets.top - 1},
|
||||||
top: paddingTop.paddingTop + insets.top - 1,
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
<View
|
<View
|
||||||
style={[a.w_full, a.align_start, {maxWidth: ONBOARDING_COL_WIDTH}]}>
|
pointerEvents="box-none"
|
||||||
<Button
|
style={[
|
||||||
key={state.activeStep} // remove focus state on nav
|
a.w_full,
|
||||||
color="secondary"
|
a.align_start,
|
||||||
variant="ghost"
|
a.flex_row,
|
||||||
shape="square"
|
a.justify_between,
|
||||||
size="small"
|
{maxWidth: ONBOARDING_COL_WIDTH},
|
||||||
label={_(msg`Go back to previous step`)}
|
]}>
|
||||||
style={[a.absolute]}
|
{state.hasPrev ? (
|
||||||
onPress={() => dispatch({type: 'prev'})}>
|
<Button
|
||||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
key={state.activeStep} // remove focus state on nav
|
||||||
</Button>
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
shape="square"
|
||||||
|
size="small"
|
||||||
|
label={_(msg`Go back to previous step`)}
|
||||||
|
onPress={() => dispatch({type: 'prev'})}
|
||||||
|
style={[a.bg_transparent]}>
|
||||||
|
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<View />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OnboardingHeaderSlot.Outlet />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -125,7 +148,8 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
{paddingTop: 17, maxWidth: '60%'},
|
a.align_center,
|
||||||
|
{height: HEADER_SLOT_SIZE, maxWidth: '60%'},
|
||||||
]}>
|
]}>
|
||||||
{Array(state.totalSteps)
|
{Array(state.totalSteps)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
@@ -149,10 +173,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View style={[a.w_full, a.mb_5xl, a.pt_md]}>{children}</View>
|
||||||
style={[a.w_full, a.mb_5xl, {paddingTop: gtMobile ? 20 : 40}]}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{height: 100 + footerHeight}} />
|
<View style={{height: 100 + footerHeight}} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import React from 'react'
|
import {useCallback, useContext, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
LayoutAnimationConfig,
|
||||||
|
SlideInRight,
|
||||||
|
SlideOutLeft,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
import {
|
import {
|
||||||
type AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
type AppBskyActorProfile,
|
type AppBskyActorProfile,
|
||||||
@@ -22,6 +29,7 @@ import {
|
|||||||
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
||||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
|
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
|
||||||
import {getAllListMembers} from '#/state/queries/list-members'
|
import {getAllListMembers} from '#/state/queries/list-members'
|
||||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||||
@@ -36,11 +44,19 @@ import {
|
|||||||
import {
|
import {
|
||||||
DescriptionText,
|
DescriptionText,
|
||||||
OnboardingControls,
|
OnboardingControls,
|
||||||
|
OnboardingHeaderSlot,
|
||||||
TitleText,
|
TitleText,
|
||||||
} from '#/screens/Onboarding/Layout'
|
} from '#/screens/Onboarding/Layout'
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
import {Context, type OnboardingState} from '#/screens/Onboarding/state'
|
||||||
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {
|
||||||
|
atoms as a,
|
||||||
|
native,
|
||||||
|
platform,
|
||||||
|
tokens,
|
||||||
|
useBreakpoints,
|
||||||
|
useTheme,
|
||||||
|
} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
import {IconCircle} from '#/components/IconCircle'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
@@ -53,10 +69,9 @@ import * as bsky from '#/types/bsky'
|
|||||||
|
|
||||||
export function StepFinished() {
|
export function StepFinished() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const {state, dispatch} = useContext(Context)
|
||||||
const {state, dispatch} = React.useContext(Context)
|
|
||||||
const onboardDispatch = useOnboardingDispatch()
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
const [saving, setSaving] = React.useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const requestNotificationsPermission = useRequestNotificationsPermission()
|
const requestNotificationsPermission = useRequestNotificationsPermission()
|
||||||
@@ -66,7 +81,7 @@ export function StepFinished() {
|
|||||||
const {startProgressGuide} = useProgressGuideControls()
|
const {startProgressGuide} = useProgressGuideControls()
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
|
|
||||||
const finishOnboarding = React.useCallback(async () => {
|
const finishOnboarding = useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
let starterPack: AppBskyGraphDefs.StarterPackView | undefined
|
let starterPack: AppBskyGraphDefs.StarterPackView | undefined
|
||||||
@@ -245,6 +260,259 @@ export function StepFinished() {
|
|||||||
gate,
|
gate,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
return state.experiments?.onboarding_value_prop ? (
|
||||||
|
<ValueProposition
|
||||||
|
finishOnboarding={finishOnboarding}
|
||||||
|
saving={saving}
|
||||||
|
state={state}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LegacyFinalStep
|
||||||
|
finishOnboarding={finishOnboarding}
|
||||||
|
saving={saving}
|
||||||
|
state={state}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROP_1 = {
|
||||||
|
light: platform({
|
||||||
|
native: require('../../../assets/images/onboarding/value_prop_1_light.webp'),
|
||||||
|
web: require('../../../assets/images/onboarding/value_prop_1_light_borderless.webp'),
|
||||||
|
}),
|
||||||
|
dim: platform({
|
||||||
|
native: require('../../../assets/images/onboarding/value_prop_1_dim.webp'),
|
||||||
|
web: require('../../../assets/images/onboarding/value_prop_1_dim_borderless.webp'),
|
||||||
|
}),
|
||||||
|
dark: platform({
|
||||||
|
native: require('../../../assets/images/onboarding/value_prop_1_dark.webp'),
|
||||||
|
web: require('../../../assets/images/onboarding/value_prop_1_dark_borderless.webp'),
|
||||||
|
}),
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const PROP_2 = {
|
||||||
|
light: require('../../../assets/images/onboarding/value_prop_2_light.webp'),
|
||||||
|
dim: require('../../../assets/images/onboarding/value_prop_2_dim.webp'),
|
||||||
|
dark: require('../../../assets/images/onboarding/value_prop_2_dark.webp'),
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const PROP_3 = {
|
||||||
|
light: require('../../../assets/images/onboarding/value_prop_3_light.webp'),
|
||||||
|
dim: require('../../../assets/images/onboarding/value_prop_3_dim.webp'),
|
||||||
|
dark: require('../../../assets/images/onboarding/value_prop_3_dark.webp'),
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function ValueProposition({
|
||||||
|
finishOnboarding,
|
||||||
|
saving,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
finishOnboarding: () => void
|
||||||
|
saving: boolean
|
||||||
|
state: OnboardingState
|
||||||
|
}) {
|
||||||
|
const [subStep, setSubStep] = useState<0 | 1 | 2>(0)
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
|
const image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][subStep]
|
||||||
|
|
||||||
|
const onPress = () => {
|
||||||
|
if (subStep === 2) {
|
||||||
|
finishOnboarding() // has its own metrics
|
||||||
|
} else if (subStep === 1) {
|
||||||
|
setSubStep(2)
|
||||||
|
logger.metric('onboarding:valueProp:stepTwo:nextPressed', {})
|
||||||
|
} else if (subStep === 0) {
|
||||||
|
setSubStep(1)
|
||||||
|
logger.metric('onboarding:valueProp:stepOne:nextPressed', {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {title, description, alt} = [
|
||||||
|
{
|
||||||
|
title: _(msg`Free your feed`),
|
||||||
|
description: _(
|
||||||
|
msg`No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you.`,
|
||||||
|
),
|
||||||
|
alt: _(
|
||||||
|
msg`A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: _(msg`Find your people`),
|
||||||
|
description: _(
|
||||||
|
msg`Ditch the trolls and clickbait. Find real people and conversations that matter to you.`,
|
||||||
|
),
|
||||||
|
alt: _(
|
||||||
|
msg`Your profile picture surrounded by concentric circles of other users' profile pictures`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: _(msg`Forget the noise`),
|
||||||
|
description: _(
|
||||||
|
msg`No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention.`,
|
||||||
|
),
|
||||||
|
alt: _(
|
||||||
|
msg`An illustration of several Bluesky posts alongside repost, like, and comment icons`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
][subStep]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!gtMobile && (
|
||||||
|
<OnboardingHeaderSlot.Portal>
|
||||||
|
<Button
|
||||||
|
disabled={saving}
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
label={_(msg`Skip introduction and start using your account`)}
|
||||||
|
onPress={() => {
|
||||||
|
logger.metric('onboarding:valueProp:skipPressed', {})
|
||||||
|
finishOnboarding()
|
||||||
|
}}
|
||||||
|
style={[a.bg_transparent]}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Skip</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</OnboardingHeaderSlot.Portal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LayoutAnimationConfig skipEntering skipExiting>
|
||||||
|
<Animated.View
|
||||||
|
key={subStep}
|
||||||
|
entering={native(
|
||||||
|
SlideInRight.easing(Easing.out(Easing.exp)).duration(500),
|
||||||
|
)}
|
||||||
|
exiting={native(
|
||||||
|
SlideOutLeft.easing(Easing.out(Easing.exp)).duration(500),
|
||||||
|
)}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
isNative && {marginHorizontal: tokens.space.xl * -1},
|
||||||
|
a.pointer_events_none,
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={image}
|
||||||
|
style={[a.w_full, {aspectRatio: 1}]}
|
||||||
|
alt={alt}
|
||||||
|
accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background
|
||||||
|
/>
|
||||||
|
{subStep === 1 && (
|
||||||
|
<Image
|
||||||
|
source={state.profileStepResults.imageUri}
|
||||||
|
style={[
|
||||||
|
a.z_10,
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
width: `${(80 / 393) * 100}%`,
|
||||||
|
height: `${(80 / 393) * 100}%`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
alt={_(msg`Your profile picture`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.mt_4xl, a.gap_2xl, a.align_center]}>
|
||||||
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
|
<Dot active={subStep === 0} />
|
||||||
|
<Dot active={subStep === 1} />
|
||||||
|
<Dot active={subStep === 2} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.font_heavy, a.text_3xl, a.text_center]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_center,
|
||||||
|
]}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</LayoutAnimationConfig>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<View style={gtMobile && [a.gap_md, a.flex_row]}>
|
||||||
|
{gtMobile && (
|
||||||
|
<Button
|
||||||
|
disabled={saving}
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Skip introduction and start using your account`)}
|
||||||
|
onPress={() => finishOnboarding()}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Skip</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
disabled={saving}
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
label={
|
||||||
|
subStep === 2
|
||||||
|
? _(msg`Complete onboarding and start using your account`)
|
||||||
|
: _(msg`Next`)
|
||||||
|
}
|
||||||
|
onPress={onPress}>
|
||||||
|
<ButtonText>
|
||||||
|
{saving ? <Trans>Finalizing</Trans> : <Trans>Next</Trans>}
|
||||||
|
</ButtonText>
|
||||||
|
{saving && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dot({active}: {active: boolean}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
{width: 8, height: 8},
|
||||||
|
active
|
||||||
|
? {backgroundColor: t.palette.primary_500}
|
||||||
|
: t.atoms.bg_contrast_50,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyFinalStep({
|
||||||
|
finishOnboarding,
|
||||||
|
saving,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
finishOnboarding: () => void
|
||||||
|
saving: boolean
|
||||||
|
state: OnboardingState
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.align_start]}>
|
<View style={[a.align_start]}>
|
||||||
<IconCircle icon={Check} style={[a.mb_2xl]} />
|
<IconCircle icon={Check} style={[a.mb_2xl]} />
|
||||||
@@ -305,7 +573,6 @@ export function StepFinished() {
|
|||||||
<Button
|
<Button
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
key={state.activeStep} // remove focus state on nav
|
key={state.activeStep} // remove focus state on nav
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
label={_(msg`Complete onboarding and start using your account`)}
|
label={_(msg`Complete onboarding and start using your account`)}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export function StepProfile() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
<OnboardingControls.Portal>
|
||||||
<View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}>
|
<View style={[a.gap_md, gtMobile && a.flex_row_reverse]}>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
|
import {
|
||||||
|
Layout,
|
||||||
|
OnboardingControls,
|
||||||
|
OnboardingHeaderSlot,
|
||||||
|
} 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'
|
||||||
@@ -14,12 +18,14 @@ import {StepSuggestedAccounts} from './StepSuggestedAccounts'
|
|||||||
export function Onboarding() {
|
export function Onboarding() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
|
const showValueProp = gate('onboarding_value_prop')
|
||||||
const showSuggestedAccounts = gate('onboarding_suggested_accounts')
|
const showSuggestedAccounts = gate('onboarding_suggested_accounts')
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
...initialState,
|
...initialState,
|
||||||
totalSteps: showSuggestedAccounts ? 4 : 3,
|
totalSteps: showSuggestedAccounts ? 4 : 3,
|
||||||
experiments: {
|
experiments: {
|
||||||
onboarding_suggested_accounts: showSuggestedAccounts,
|
onboarding_suggested_accounts: showSuggestedAccounts,
|
||||||
|
onboarding_value_prop: showValueProp,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,20 +59,22 @@ export function Onboarding() {
|
|||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<OnboardingControls.Provider>
|
<OnboardingControls.Provider>
|
||||||
<Context.Provider
|
<OnboardingHeaderSlot.Provider>
|
||||||
value={useMemo(
|
<Context.Provider
|
||||||
() => ({state, dispatch, interestsDisplayNames}),
|
value={useMemo(
|
||||||
[state, dispatch, interestsDisplayNames],
|
() => ({state, dispatch, interestsDisplayNames}),
|
||||||
)}>
|
[state, dispatch, interestsDisplayNames],
|
||||||
<Layout>
|
)}>
|
||||||
{state.activeStep === 'profile' && <StepProfile />}
|
<Layout>
|
||||||
{state.activeStep === 'interests' && <StepInterests />}
|
{state.activeStep === 'profile' && <StepProfile />}
|
||||||
{state.activeStep === 'suggested-accounts' && (
|
{state.activeStep === 'interests' && <StepInterests />}
|
||||||
<StepSuggestedAccounts />
|
{state.activeStep === 'suggested-accounts' && (
|
||||||
)}
|
<StepSuggestedAccounts />
|
||||||
{state.activeStep === 'finished' && <StepFinished />}
|
)}
|
||||||
</Layout>
|
{state.activeStep === 'finished' && <StepFinished />}
|
||||||
</Context.Provider>
|
</Layout>
|
||||||
|
</Context.Provider>
|
||||||
|
</OnboardingHeaderSlot.Provider>
|
||||||
</OnboardingControls.Provider>
|
</OnboardingControls.Provider>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type OnboardingState = {
|
|||||||
|
|
||||||
experiments?: {
|
experiments?: {
|
||||||
onboarding_suggested_accounts?: boolean
|
onboarding_suggested_accounts?: boolean
|
||||||
|
onboarding_value_prop?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||