diff --git a/assets/images/onboarding/value_prop_1_dark.webp b/assets/images/onboarding/value_prop_1_dark.webp new file mode 100644 index 0000000000..259e6155b2 Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dark.webp differ diff --git a/assets/images/onboarding/value_prop_1_dark_borderless.webp b/assets/images/onboarding/value_prop_1_dark_borderless.webp new file mode 100644 index 0000000000..5c44735f9b Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dark_borderless.webp differ diff --git a/assets/images/onboarding/value_prop_1_dim.webp b/assets/images/onboarding/value_prop_1_dim.webp new file mode 100644 index 0000000000..2fea1b9916 Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dim.webp differ diff --git a/assets/images/onboarding/value_prop_1_dim_borderless.webp b/assets/images/onboarding/value_prop_1_dim_borderless.webp new file mode 100644 index 0000000000..98c7542b44 Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dim_borderless.webp differ diff --git a/assets/images/onboarding/value_prop_1_light.webp b/assets/images/onboarding/value_prop_1_light.webp new file mode 100644 index 0000000000..13ffbf02b3 Binary files /dev/null and b/assets/images/onboarding/value_prop_1_light.webp differ diff --git a/assets/images/onboarding/value_prop_1_light_borderless.webp b/assets/images/onboarding/value_prop_1_light_borderless.webp new file mode 100644 index 0000000000..1cf0cd032e Binary files /dev/null and b/assets/images/onboarding/value_prop_1_light_borderless.webp differ diff --git a/assets/images/onboarding/value_prop_2_dark.webp b/assets/images/onboarding/value_prop_2_dark.webp new file mode 100644 index 0000000000..7a60d554eb Binary files /dev/null and b/assets/images/onboarding/value_prop_2_dark.webp differ diff --git a/assets/images/onboarding/value_prop_2_dim.webp b/assets/images/onboarding/value_prop_2_dim.webp new file mode 100644 index 0000000000..86f2c1f893 Binary files /dev/null and b/assets/images/onboarding/value_prop_2_dim.webp differ diff --git a/assets/images/onboarding/value_prop_2_light.webp b/assets/images/onboarding/value_prop_2_light.webp new file mode 100644 index 0000000000..2b385a3f2a Binary files /dev/null and b/assets/images/onboarding/value_prop_2_light.webp differ diff --git a/assets/images/onboarding/value_prop_3_dark.webp b/assets/images/onboarding/value_prop_3_dark.webp new file mode 100644 index 0000000000..0ea0dba1c1 Binary files /dev/null and b/assets/images/onboarding/value_prop_3_dark.webp differ diff --git a/assets/images/onboarding/value_prop_3_dim.webp b/assets/images/onboarding/value_prop_3_dim.webp new file mode 100644 index 0000000000..a0986e94be Binary files /dev/null and b/assets/images/onboarding/value_prop_3_dim.webp differ diff --git a/assets/images/onboarding/value_prop_3_light.webp b/assets/images/onboarding/value_prop_3_light.webp new file mode 100644 index 0000000000..cd8bcbb457 Binary files /dev/null and b/assets/images/onboarding/value_prop_3_light.webp differ diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 7fe8320109..9a9a739a98 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -8,6 +8,7 @@ export type Gate = | 'old_postonboarding' | 'onboarding_add_video_feed' | 'onboarding_suggested_accounts' + | 'onboarding_value_prop' | 'post_follow_profile_suggested_accounts' | 'post_threads_v2_unspecced' | 'remove_show_latest_button' diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index ea6e2fb5eb..1cb4eb9d36 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -125,6 +125,9 @@ export type MetricEvents = { 'onboarding:finished:avatarResult': { avatarResult: 'default' | 'created' | 'uploaded' } + 'onboarding:valueProp:stepOne:nextPressed': {} + 'onboarding:valueProp:stepTwo:nextPressed': {} + 'onboarding:valueProp:skipPressed': {} 'home:feedDisplayed': { feedUrl: string feedType: string diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx index 13dec54893..6394d9c969 100644 --- a/src/screens/Onboarding/Layout.tsx +++ b/src/screens/Onboarding/Layout.tsx @@ -20,12 +20,14 @@ import { import {leading} from '#/alf/typography' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' +import {HEADER_SLOT_SIZE} from '#/components/Layout' import {createPortalGroup} from '#/components/Portal' import {P, Text} from '#/components/Typography' const ONBOARDING_COL_WIDTH = 420 export const OnboardingControls = createPortalGroup() +export const OnboardingHeaderSlot = createPortalGroup() export function Layout({children}: React.PropsWithChildren<{}>) { const {_} = useLingui() @@ -65,46 +67,67 @@ export function Layout({children}: React.PropsWithChildren<{}>) { t.atoms.bg, ]}> {__DEV__ && ( - - - + )} - {!gtMobile && state.hasPrev && ( + {!gtMobile && ( - + pointerEvents="box-none" + style={[ + a.w_full, + a.align_start, + a.flex_row, + a.justify_between, + {maxWidth: ONBOARDING_COL_WIDTH}, + ]}> + {state.hasPrev ? ( + + ) : ( + + )} + + )} @@ -125,7 +148,8 @@ export function Layout({children}: React.PropsWithChildren<{}>) { a.flex_row, a.gap_sm, a.w_full, - {paddingTop: 17, maxWidth: '60%'}, + a.align_center, + {height: HEADER_SLOT_SIZE, maxWidth: '60%'}, ]}> {Array(state.totalSteps) .fill(0) @@ -149,10 +173,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) { - - {children} - + {children} diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index 54d282a5ea..3ee1fa3d1a 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -1,5 +1,12 @@ -import React from 'react' +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, @@ -22,6 +29,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 {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' @@ -36,11 +44,19 @@ import { import { DescriptionText, OnboardingControls, + OnboardingHeaderSlot, TitleText, } 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 {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 {IconCircle} from '#/components/IconCircle' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' @@ -53,10 +69,9 @@ import * as bsky from '#/types/bsky' export function StepFinished() { const {_} = useLingui() - const t = useTheme() - const {state, dispatch} = React.useContext(Context) + const {state, dispatch} = useContext(Context) const onboardDispatch = useOnboardingDispatch() - const [saving, setSaving] = React.useState(false) + const [saving, setSaving] = useState(false) const queryClient = useQueryClient() const agent = useAgent() const requestNotificationsPermission = useRequestNotificationsPermission() @@ -66,7 +81,7 @@ export function StepFinished() { const {startProgressGuide} = useProgressGuideControls() const gate = useGate() - const finishOnboarding = React.useCallback(async () => { + const finishOnboarding = useCallback(async () => { setSaving(true) let starterPack: AppBskyGraphDefs.StarterPackView | undefined @@ -245,6 +260,259 @@ export function StepFinished() { gate, ]) + return state.experiments?.onboarding_value_prop ? ( + + ) : ( + + ) +} + +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 && ( + + + + )} + + + + + {alt} + {subStep === 1 && ( + {_(msg`Your + )} + + + + + + + + + + + + {title} + + + {description} + + + + + + + + + {gtMobile && ( + + )} + + + + + ) +} + +function Dot({active}: {active: boolean}) { + const t = useTheme() + const {_} = useLingui() + + return ( + + ) +} + +function LegacyFinalStep({ + finishOnboarding, + saving, + state, +}: { + finishOnboarding: () => void + saving: boolean + state: OnboardingState +}) { + const t = useTheme() + const {_} = useLingui() + return ( @@ -305,7 +573,6 @@ export function StepFinished() {