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 && (
+
+
+
+ )}
+
+
+
+
+
+ {subStep === 1 && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {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() {