diff --git a/assets/images/onboarding/value_prop_1_dark.webp b/assets/images/onboarding/value_prop_1_dark.webp
index 259e6155b2..28dc7c18d3 100644
Binary files a/assets/images/onboarding/value_prop_1_dark.webp 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
index 5c44735f9b..733bd4ccd7 100644
Binary files a/assets/images/onboarding/value_prop_1_dark_borderless.webp 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
index 2fea1b9916..614a16f361 100644
Binary files a/assets/images/onboarding/value_prop_1_dim.webp 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
index 98c7542b44..f17dc1975e 100644
Binary files a/assets/images/onboarding/value_prop_1_dim_borderless.webp 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
index 13ffbf02b3..fd48f3a42f 100644
Binary files a/assets/images/onboarding/value_prop_1_light.webp 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
index 1cf0cd032e..f1917c5f20 100644
Binary files a/assets/images/onboarding/value_prop_1_light_borderless.webp and b/assets/images/onboarding/value_prop_1_light_borderless.webp differ
diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx
index c815bde844..a3627a179e 100644
--- a/src/screens/Onboarding/Layout.tsx
+++ b/src/screens/Onboarding/Layout.tsx
@@ -173,7 +173,9 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
- {children}
+
+ {children}
+
diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx b/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx
new file mode 100644
index 0000000000..d3773880b0
--- /dev/null
+++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx
@@ -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 (
+
+ )
+}
diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx b/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx
new file mode 100644
index 0000000000..56d0efe114
--- /dev/null
+++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx
@@ -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(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 (
+
+ {
+ const page = evt.nativeEvent.position as 0 | 1 | 2
+ if (step !== page) {
+ setActivePage(page)
+ setStep(page)
+ }
+ }}>
+ {([0, 1, 2] as const).map(page => (
+
+ ))}
+
+
+ )
+}
+
+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 (
+
+
+
+ {page === 1 && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {title}
+
+ {description}
+
+
+
+
+ )
+}
diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx b/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx
new file mode 100644
index 0000000000..f90cec0fa3
--- /dev/null
+++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx
@@ -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 (
+
+
+
+ {step === 1 && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {title}
+
+ {description}
+
+
+
+
+ )
+}
diff --git a/src/screens/Onboarding/StepFinished/images.ts b/src/screens/Onboarding/StepFinished/images.ts
new file mode 100644
index 0000000000..48a68a1326
--- /dev/null
+++ b/src/screens/Onboarding/StepFinished/images.ts
@@ -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
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished/index.tsx
similarity index 73%
rename from src/screens/Onboarding/StepFinished.tsx
rename to src/screens/Onboarding/StepFinished/index.tsx
index 3ed268a575..d2ccb9db91 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished/index.tsx
@@ -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({
)}
-
-
-
-
- {subStep === 1 && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {title}
-
-
- {description}
-
-
-
-
-
+ setSubStep(ss)}
+ avatarUri={state.profileStepResults.imageUri}
+ />
- {gtMobile && (
+ {gtMobile && (isWeb ? subStep !== 2 : true) && (
- )
-}
-
function LegacyFinalStep({
finishOnboarding,
saving,