Value prop screen - polish, convert to pager (#9133)
* turn value prop screen into a pager on native * rm ts-ingore * fix pager swipe on android --------- Co-authored-by: Eric Bailey <git@esb.lol>
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 125 KiB |
@@ -5,7 +5,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
@@ -59,13 +59,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
aria-label={dialogLabel}
|
||||
accessibilityLabel={dialogLabel}
|
||||
accessibilityHint={_(msg`Customizes your Bluesky experience`)}
|
||||
style={[
|
||||
// @ts-ignore web only -prf
|
||||
isWeb ? a.fixed : a.absolute,
|
||||
a.inset_0,
|
||||
a.flex_1,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
|
||||
{__DEV__ && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -135,7 +129,8 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
<ScrollView
|
||||
ref={scrollview}
|
||||
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
||||
contentContainerStyle={{borderWidth: 0}}
|
||||
contentContainerStyle={{borderWidth: 0, minHeight: '100%'}}
|
||||
showsVerticalScrollIndicator={!isAndroid}
|
||||
scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
|
||||
// @ts-expect-error web only --prf
|
||||
dataSet={{'stable-gutters': 1}}>
|
||||
@@ -173,7 +168,9 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.w_full, a.mb_5xl, a.pt_md]}>{children}</View>
|
||||
<View style={[a.w_full, a.h_full, a.mb_5xl, a.pt_md]}>
|
||||
{children}
|
||||
</View>
|
||||
|
||||
<View style={{height: 100 + footerHeight}} />
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
export function useValuePropText(step: 0 | 1 | 2) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return [
|
||||
{
|
||||
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`,
|
||||
),
|
||||
},
|
||||
][step]
|
||||
}
|
||||
|
||||
export function Dot({active}: {active: boolean}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
{width: 8, height: 8},
|
||||
active
|
||||
? {backgroundColor: t.palette.primary_500}
|
||||
: t.atoms.bg_contrast_50,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import {useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import PagerView from 'react-native-pager-view'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PROP_1, PROP_2, PROP_3} from './images'
|
||||
import {Dot, useValuePropText} from './ValuePropositionPager.shared'
|
||||
|
||||
export function ValuePropositionPager({
|
||||
step,
|
||||
setStep,
|
||||
avatarUri,
|
||||
}: {
|
||||
step: 0 | 1 | 2
|
||||
setStep: (step: 0 | 1 | 2) => void
|
||||
avatarUri?: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const [activePage, setActivePage] = useState(step)
|
||||
const ref = useRef<PagerView>(null)
|
||||
|
||||
if (step !== activePage) {
|
||||
setActivePage(step)
|
||||
ref.current?.setPage(step)
|
||||
}
|
||||
|
||||
const images = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]]
|
||||
|
||||
return (
|
||||
<View style={[a.h_full, {marginHorizontal: tokens.space.xl * -1}]}>
|
||||
<PagerView
|
||||
ref={ref}
|
||||
style={[a.flex_1]}
|
||||
initialPage={step}
|
||||
onPageSelected={evt => {
|
||||
const page = evt.nativeEvent.position as 0 | 1 | 2
|
||||
if (step !== page) {
|
||||
setActivePage(page)
|
||||
setStep(page)
|
||||
}
|
||||
}}>
|
||||
{([0, 1, 2] as const).map(page => (
|
||||
<Page
|
||||
key={page}
|
||||
page={page}
|
||||
image={images[page]}
|
||||
avatarUri={avatarUri}
|
||||
/>
|
||||
))}
|
||||
</PagerView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Page({
|
||||
page,
|
||||
image,
|
||||
avatarUri,
|
||||
}: {
|
||||
page: 0 | 1 | 2
|
||||
image: string
|
||||
avatarUri?: string
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {title, description, alt} = useValuePropText(page)
|
||||
|
||||
return (
|
||||
<View key={page}>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.pointer_events_none,
|
||||
]}>
|
||||
<Image
|
||||
source={image}
|
||||
style={[a.w_full, a.aspect_square]}
|
||||
alt={alt}
|
||||
accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background
|
||||
/>
|
||||
{page === 1 && (
|
||||
<Image
|
||||
source={avatarUri}
|
||||
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.px_xl, a.align_center]}>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<Dot active={page === 0} />
|
||||
<Dot active={page === 1} />
|
||||
<Dot active={page === 2} />
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_bold, 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>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PROP_1, PROP_2, PROP_3} from './images'
|
||||
import {Dot, useValuePropText} from './ValuePropositionPager.shared'
|
||||
|
||||
export function ValuePropositionPager({
|
||||
step,
|
||||
avatarUri,
|
||||
}: {
|
||||
step: 0 | 1 | 2
|
||||
avatarUri?: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][step]
|
||||
|
||||
const {title, description, alt} = useValuePropText(step)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
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
|
||||
/>
|
||||
{step === 1 && (
|
||||
<Image
|
||||
source={avatarUri}
|
||||
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={step === 0} />
|
||||
<Dot active={step === 1} />
|
||||
<Dot active={step === 2} />
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_bold, 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>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {platform} from '#/alf'
|
||||
|
||||
export 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
|
||||
|
||||
export 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
|
||||
|
||||
export 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
|
||||
@@ -1,12 +1,5 @@
|
||||
import {useCallback, useContext, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {
|
||||
Easing,
|
||||
LayoutAnimationConfig,
|
||||
SlideInRight,
|
||||
SlideOutLeft,
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyActorProfile,
|
||||
@@ -29,7 +22,7 @@ import {
|
||||
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
|
||||
import {getAllListMembers} from '#/state/queries/list-members'
|
||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||
@@ -49,14 +42,7 @@ import {
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
import {Context, type OnboardingState} from '#/screens/Onboarding/state'
|
||||
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||
import {
|
||||
atoms as a,
|
||||
native,
|
||||
platform,
|
||||
tokens,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {IconCircle} from '#/components/IconCircle'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
|
||||
@@ -67,6 +53,7 @@ import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {ValuePropositionPager} from './ValuePropositionPager'
|
||||
|
||||
export function StepFinished() {
|
||||
const {state, dispatch} = useContext(Context)
|
||||
@@ -275,33 +262,6 @@ export function StepFinished() {
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -312,12 +272,9 @@ function ValueProposition({
|
||||
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
|
||||
@@ -330,36 +287,6 @@ function ValueProposition({
|
||||
}
|
||||
}
|
||||
|
||||
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 && (
|
||||
@@ -382,75 +309,15 @@ function ValueProposition({
|
||||
</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, a.aspect_square]}
|
||||
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_bold, 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>
|
||||
<ValuePropositionPager
|
||||
step={subStep}
|
||||
setStep={ss => setSubStep(ss)}
|
||||
avatarUri={state.profileStepResults.imageUri}
|
||||
/>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<View style={gtMobile && [a.gap_md, a.flex_row]}>
|
||||
{gtMobile && (
|
||||
{gtMobile && (isWeb ? subStep !== 2 : true) && (
|
||||
<Button
|
||||
disabled={saving}
|
||||
color="secondary"
|
||||
@@ -492,22 +359,6 @@ function ValueProposition({
|
||||
)
|
||||
}
|
||||
|
||||
function Dot({active}: {active: boolean}) {
|
||||
const t = useTheme()
|
||||
|
||||
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,
|
||||