about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--assets/images/onboarding/value_prop_1_dark.webpbin0 -> 128556 bytes
-rw-r--r--assets/images/onboarding/value_prop_1_dark_borderless.webpbin0 -> 131872 bytes
-rw-r--r--assets/images/onboarding/value_prop_1_dim.webpbin0 -> 112990 bytes
-rw-r--r--assets/images/onboarding/value_prop_1_dim_borderless.webpbin0 -> 130520 bytes
-rw-r--r--assets/images/onboarding/value_prop_1_light.webpbin0 -> 120414 bytes
-rw-r--r--assets/images/onboarding/value_prop_1_light_borderless.webpbin0 -> 122448 bytes
-rw-r--r--assets/images/onboarding/value_prop_2_dark.webpbin0 -> 203032 bytes
-rw-r--r--assets/images/onboarding/value_prop_2_dim.webpbin0 -> 200058 bytes
-rw-r--r--assets/images/onboarding/value_prop_2_light.webpbin0 -> 155674 bytes
-rw-r--r--assets/images/onboarding/value_prop_3_dark.webpbin0 -> 143288 bytes
-rw-r--r--assets/images/onboarding/value_prop_3_dim.webpbin0 -> 137526 bytes
-rw-r--r--assets/images/onboarding/value_prop_3_light.webpbin0 -> 105518 bytes
-rw-r--r--src/alf/atoms.ts28
-rw-r--r--src/components/InterestTabs.tsx390
-rw-r--r--src/components/ProgressGuide/FollowDialog.tsx146
-rw-r--r--src/lib/hooks/useDraggableScrollView.ts6
-rw-r--r--src/lib/statsig/gates.ts2
-rw-r--r--src/logger/metrics.ts19
-rw-r--r--src/screens/Onboarding/Layout.tsx126
-rw-r--r--src/screens/Onboarding/StepFinished.tsx292
-rw-r--r--src/screens/Onboarding/StepInterests/index.tsx11
-rw-r--r--src/screens/Onboarding/StepProfile/index.tsx2
-rw-r--r--src/screens/Onboarding/StepSuggestedAccounts/index.tsx356
-rw-r--r--src/screens/Onboarding/index.tsx49
-rw-r--r--src/screens/Onboarding/state.ts58
-rw-r--r--src/screens/Onboarding/util.ts14
-rw-r--r--src/screens/Search/Explore.tsx2
-rw-r--r--src/screens/Search/modules/ExploreSuggestedAccounts.tsx74
-rw-r--r--src/screens/Search/util/useSuggestedUsers.ts9
-rw-r--r--src/state/queries/trending/useGetSuggestedUsersQuery.ts9
30 files changed, 1302 insertions, 291 deletions
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 000000000..259e6155b
--- /dev/null
+++ b/assets/images/onboarding/value_prop_1_dark.webp
Binary files differdiff --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 000000000..5c44735f9
--- /dev/null
+++ b/assets/images/onboarding/value_prop_1_dark_borderless.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_1_dim.webp b/assets/images/onboarding/value_prop_1_dim.webp
new file mode 100644
index 000000000..2fea1b991
--- /dev/null
+++ b/assets/images/onboarding/value_prop_1_dim.webp
Binary files differdiff --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 000000000..98c7542b4
--- /dev/null
+++ b/assets/images/onboarding/value_prop_1_dim_borderless.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_1_light.webp b/assets/images/onboarding/value_prop_1_light.webp
new file mode 100644
index 000000000..13ffbf02b
--- /dev/null
+++ b/assets/images/onboarding/value_prop_1_light.webp
Binary files differdiff --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 000000000..1cf0cd032
--- /dev/null
+++ b/assets/images/onboarding/value_prop_1_light_borderless.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_2_dark.webp b/assets/images/onboarding/value_prop_2_dark.webp
new file mode 100644
index 000000000..7a60d554e
--- /dev/null
+++ b/assets/images/onboarding/value_prop_2_dark.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_2_dim.webp b/assets/images/onboarding/value_prop_2_dim.webp
new file mode 100644
index 000000000..86f2c1f89
--- /dev/null
+++ b/assets/images/onboarding/value_prop_2_dim.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_2_light.webp b/assets/images/onboarding/value_prop_2_light.webp
new file mode 100644
index 000000000..2b385a3f2
--- /dev/null
+++ b/assets/images/onboarding/value_prop_2_light.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_3_dark.webp b/assets/images/onboarding/value_prop_3_dark.webp
new file mode 100644
index 000000000..0ea0dba1c
--- /dev/null
+++ b/assets/images/onboarding/value_prop_3_dark.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_3_dim.webp b/assets/images/onboarding/value_prop_3_dim.webp
new file mode 100644
index 000000000..a0986e94b
--- /dev/null
+++ b/assets/images/onboarding/value_prop_3_dim.webp
Binary files differdiff --git a/assets/images/onboarding/value_prop_3_light.webp b/assets/images/onboarding/value_prop_3_light.webp
new file mode 100644
index 000000000..cd8bcbb45
--- /dev/null
+++ b/assets/images/onboarding/value_prop_3_light.webp
Binary files differdiff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 5b7c5c87e..c0f959ec8 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -70,9 +70,21 @@ export const atoms = {
   overflow_visible: {
     overflow: 'visible',
   },
+  overflow_x_visible: {
+    overflowX: 'visible',
+  },
+  overflow_y_visible: {
+    overflowY: 'visible',
+  },
   overflow_hidden: {
     overflow: 'hidden',
   },
+  overflow_x_hidden: {
+    overflowX: 'hidden',
+  },
+  overflow_y_hidden: {
+    overflowY: 'hidden',
+  },
   /**
    * @platform web
    */
@@ -363,6 +375,14 @@ export const atoms = {
   border_r_0: {
     borderRightWidth: 0,
   },
+  border_x_0: {
+    borderLeftWidth: 0,
+    borderRightWidth: 0,
+  },
+  border_y_0: {
+    borderTopWidth: 0,
+    borderBottomWidth: 0,
+  },
   border: {
     borderWidth: StyleSheet.hairlineWidth,
   },
@@ -378,6 +398,14 @@ export const atoms = {
   border_r: {
     borderRightWidth: StyleSheet.hairlineWidth,
   },
+  border_x: {
+    borderLeftWidth: StyleSheet.hairlineWidth,
+    borderRightWidth: StyleSheet.hairlineWidth,
+  },
+  border_y: {
+    borderTopWidth: StyleSheet.hairlineWidth,
+    borderBottomWidth: StyleSheet.hairlineWidth,
+  },
   border_transparent: {
     borderColor: 'transparent',
   },
diff --git a/src/components/InterestTabs.tsx b/src/components/InterestTabs.tsx
new file mode 100644
index 000000000..b61157ed8
--- /dev/null
+++ b/src/components/InterestTabs.tsx
@@ -0,0 +1,390 @@
+import {useEffect, useRef, useState} from 'react'
+import {
+  type ScrollView,
+  type StyleProp,
+  View,
+  type ViewStyle,
+} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {isWeb} from '#/platform/detection'
+import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
+import {atoms as a, tokens, useTheme, web} from '#/alf'
+import {transparentifyColor} from '#/alf/util/colorGeneration'
+import {Button, ButtonIcon} from '#/components/Button'
+import {
+  ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft,
+  ArrowRight_Stroke2_Corner0_Rounded as ArrowRight,
+} from '#/components/icons/Arrow'
+import {Text} from '#/components/Typography'
+
+/**
+ * Tab component that automatically scrolls the selected tab into view - used for interests
+ * in the Find Follows dialog, Explore screen, etc.
+ */
+export function InterestTabs({
+  onSelectTab,
+  interests,
+  selectedInterest,
+  disabled,
+  interestsDisplayNames,
+  TabComponent = Tab,
+  contentContainerStyle,
+  gutterWidth = tokens.space.lg,
+}: {
+  onSelectTab: (tab: string) => void
+  interests: string[]
+  selectedInterest: string
+  interestsDisplayNames: Record<string, string>
+  /** still allows changing tab, but removes the active state from the selected tab */
+  disabled?: boolean
+  TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
+  contentContainerStyle?: StyleProp<ViewStyle>
+  gutterWidth?: number
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const listRef = useRef<ScrollView>(null)
+  const [totalWidth, setTotalWidth] = useState(0)
+  const [scrollX, setScrollX] = useState(0)
+  const [contentWidth, setContentWidth] = useState(0)
+  const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
+  const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
+
+  const onInitialLayout = useNonReactiveCallback(() => {
+    const index = interests.indexOf(selectedInterest)
+    scrollIntoViewIfNeeded(index)
+  })
+
+  useEffect(() => {
+    if (tabOffsets) {
+      onInitialLayout()
+    }
+  }, [tabOffsets, onInitialLayout])
+
+  function scrollIntoViewIfNeeded(index: number) {
+    const btnLayout = tabOffsets[index]
+    if (!btnLayout) return
+    listRef.current?.scrollTo({
+      // centered
+      x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
+      animated: true,
+    })
+  }
+
+  function handleSelectTab(index: number) {
+    const tab = interests[index]
+    onSelectTab(tab)
+    scrollIntoViewIfNeeded(index)
+  }
+
+  function handleTabLayout(index: number, x: number, width: number) {
+    if (!tabOffsets.length) {
+      pendingTabOffsets.current[index] = {x, width}
+      if (pendingTabOffsets.current.length === interests.length) {
+        setTabOffsets(pendingTabOffsets.current)
+      }
+    }
+  }
+
+  const canScrollLeft = scrollX > 0
+  const canScrollRight = scrollX < contentWidth - totalWidth
+
+  const cleanupRef = useRef<(() => void) | null>(null)
+
+  function scrollLeft() {
+    if (isContinuouslyScrollingRef.current) {
+      return
+    }
+    if (listRef.current && canScrollLeft) {
+      const newScrollX = Math.max(0, scrollX - 200)
+      listRef.current.scrollTo({x: newScrollX, animated: true})
+    }
+  }
+
+  function scrollRight() {
+    if (isContinuouslyScrollingRef.current) {
+      return
+    }
+    if (listRef.current && canScrollRight) {
+      const maxScroll = contentWidth - totalWidth
+      const newScrollX = Math.min(maxScroll, scrollX + 200)
+      listRef.current.scrollTo({x: newScrollX, animated: true})
+    }
+  }
+
+  const isContinuouslyScrollingRef = useRef(false)
+
+  function startContinuousScroll(direction: 'left' | 'right') {
+    // Clear any existing continuous scroll
+    if (cleanupRef.current) {
+      cleanupRef.current()
+    }
+
+    let holdTimeout: NodeJS.Timeout | null = null
+    let animationFrame: number | null = null
+    let isActive = true
+    isContinuouslyScrollingRef.current = false
+
+    const cleanup = () => {
+      isActive = false
+      if (holdTimeout) clearTimeout(holdTimeout)
+      if (animationFrame) cancelAnimationFrame(animationFrame)
+      cleanupRef.current = null
+      // Reset flag after a delay to prevent onPress from firing
+      setTimeout(() => {
+        isContinuouslyScrollingRef.current = false
+      }, 100)
+    }
+
+    cleanupRef.current = cleanup
+
+    // Start continuous scrolling after hold delay
+    holdTimeout = setTimeout(() => {
+      if (!isActive) return
+
+      isContinuouslyScrollingRef.current = true
+      let currentScrollPosition = scrollX
+
+      const scroll = () => {
+        if (!isActive || !listRef.current) return
+
+        const scrollAmount = 3
+        const maxScroll = contentWidth - totalWidth
+
+        let newScrollX: number
+        let canContinue = false
+
+        if (direction === 'left' && currentScrollPosition > 0) {
+          newScrollX = Math.max(0, currentScrollPosition - scrollAmount)
+          canContinue = newScrollX > 0
+        } else if (direction === 'right' && currentScrollPosition < maxScroll) {
+          newScrollX = Math.min(maxScroll, currentScrollPosition + scrollAmount)
+          canContinue = newScrollX < maxScroll
+        } else {
+          return
+        }
+
+        currentScrollPosition = newScrollX
+        listRef.current.scrollTo({x: newScrollX, animated: false})
+
+        if (canContinue && isActive) {
+          animationFrame = requestAnimationFrame(scroll)
+        }
+      }
+
+      scroll()
+    }, 500)
+  }
+
+  function stopContinuousScroll() {
+    if (cleanupRef.current) {
+      cleanupRef.current()
+    }
+  }
+
+  useEffect(() => {
+    return () => {
+      if (cleanupRef.current) {
+        cleanupRef.current()
+      }
+    }
+  }, [])
+
+  return (
+    <View style={[a.relative, a.flex_row]}>
+      <DraggableScrollView
+        ref={listRef}
+        contentContainerStyle={[
+          a.gap_sm,
+          {paddingHorizontal: gutterWidth},
+          contentContainerStyle,
+        ]}
+        showsHorizontalScrollIndicator={false}
+        decelerationRate="fast"
+        snapToOffsets={
+          tabOffsets.length === interests.length
+            ? tabOffsets.map(o => o.x - tokens.space.xl)
+            : undefined
+        }
+        onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
+        onContentSizeChange={width => setContentWidth(width)}
+        onScroll={evt => {
+          const newScrollX = evt.nativeEvent.contentOffset.x
+          setScrollX(newScrollX)
+        }}
+        scrollEventThrottle={16}>
+        {interests.map((interest, i) => {
+          const active = interest === selectedInterest && !disabled
+          return (
+            <TabComponent
+              key={interest}
+              onSelectTab={handleSelectTab}
+              active={active}
+              index={i}
+              interest={interest}
+              interestsDisplayName={interestsDisplayNames[interest]}
+              onLayout={handleTabLayout}
+            />
+          )
+        })}
+      </DraggableScrollView>
+      {isWeb && canScrollLeft && (
+        <View
+          style={[
+            a.absolute,
+            a.top_0,
+            a.left_0,
+            a.bottom_0,
+            a.justify_center,
+            {paddingLeft: gutterWidth},
+            a.pr_md,
+            a.z_10,
+            web({
+              background: `linear-gradient(to right,  ${t.atoms.bg.backgroundColor} 0%, ${t.atoms.bg.backgroundColor} 70%, ${transparentifyColor(t.atoms.bg.backgroundColor, 0)} 100%)`,
+            }),
+          ]}>
+          <Button
+            label={_(msg`Scroll left`)}
+            onPress={scrollLeft}
+            onPressIn={() => startContinuousScroll('left')}
+            onPressOut={stopContinuousScroll}
+            color="secondary"
+            size="small"
+            style={[
+              a.border,
+              t.atoms.border_contrast_low,
+              t.atoms.bg,
+              a.h_full,
+              {aspectRatio: 1},
+              a.rounded_full,
+            ]}>
+            <ButtonIcon icon={ArrowLeft} />
+          </Button>
+        </View>
+      )}
+      {isWeb && canScrollRight && (
+        <View
+          style={[
+            a.absolute,
+            a.top_0,
+            a.right_0,
+            a.bottom_0,
+            a.justify_center,
+            {paddingRight: gutterWidth},
+            a.pl_md,
+            a.z_10,
+            web({
+              background: `linear-gradient(to left, ${t.atoms.bg.backgroundColor} 0%, ${t.atoms.bg.backgroundColor} 70%, ${transparentifyColor(t.atoms.bg.backgroundColor, 0)} 100%)`,
+            }),
+          ]}>
+          <Button
+            label={_(msg`Scroll right`)}
+            onPress={scrollRight}
+            onPressIn={() => startContinuousScroll('right')}
+            onPressOut={stopContinuousScroll}
+            color="secondary"
+            size="small"
+            style={[
+              a.border,
+              t.atoms.border_contrast_low,
+              t.atoms.bg,
+              a.h_full,
+              {aspectRatio: 1},
+              a.rounded_full,
+            ]}>
+            <ButtonIcon icon={ArrowRight} />
+          </Button>
+        </View>
+      )}
+    </View>
+  )
+}
+
+function Tab({
+  onSelectTab,
+  interest,
+  active,
+  index,
+  interestsDisplayName,
+  onLayout,
+}: {
+  onSelectTab: (index: number) => void
+  interest: string
+  active: boolean
+  index: number
+  interestsDisplayName: string
+  onLayout: (index: number, x: number, width: number) => void
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const label = active
+    ? _(
+        msg({
+          message: `"${interestsDisplayName}" category (active)`,
+          comment:
+            'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is currently selected.',
+        }),
+      )
+    : _(
+        msg({
+          message: `Select "${interestsDisplayName}" category`,
+          comment:
+            'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is not currently active and can be selected.',
+        }),
+      )
+
+  return (
+    <View
+      key={interest}
+      onLayout={e =>
+        onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
+      }>
+      <Button
+        label={label}
+        onPress={() => onSelectTab(index)}
+        // disable focus ring, we handle it
+        style={web({outline: 'none'})}>
+        {({hovered, pressed, focused}) => (
+          <View
+            style={[
+              a.rounded_full,
+              a.px_lg,
+              a.py_sm,
+              a.border,
+              active || hovered || pressed
+                ? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
+                : focused
+                  ? {
+                      borderColor: t.palette.primary_300,
+                      backgroundColor: t.palette.primary_25,
+                    }
+                  : [t.atoms.bg, t.atoms.border_contrast_low],
+            ]}>
+            <Text
+              style={[
+                a.font_medium,
+                active || hovered || pressed
+                  ? t.atoms.text
+                  : t.atoms.text_contrast_medium,
+              ]}>
+              {interestsDisplayName}
+            </Text>
+          </View>
+        )}
+      </Button>
+    </View>
+  )
+}
+
+export function boostInterests(boosts?: string[]) {
+  return (_a: string, _b: string) => {
+    const indexA = boosts?.indexOf(_a) ?? -1
+    const indexB = boosts?.indexOf(_b) ?? -1
+    const rankA = indexA === -1 ? Infinity : indexA
+    const rankB = indexB === -1 ? Infinity : indexB
+    return rankA - rankB
+  }
+}
diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx
index 20ebb0abf..a2ec4df13 100644
--- a/src/components/ProgressGuide/FollowDialog.tsx
+++ b/src/components/ProgressGuide/FollowDialog.tsx
@@ -1,17 +1,9 @@
 import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
-import {
-  ScrollView,
-  type StyleProp,
-  TextInput,
-  useWindowDimensions,
-  View,
-  type ViewStyle,
-} from 'react-native'
+import {TextInput, useWindowDimensions, View} from 'react-native'
 import {type ModerationOpts} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {logEvent} from '#/lib/statsig/statsig'
 import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -28,7 +20,6 @@ import {
 import {
   atoms as a,
   native,
-  tokens,
   useBreakpoints,
   useTheme,
   type ViewStyleProp,
@@ -40,6 +31,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
 import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {boostInterests, InterestTabs} from '#/components/InterestTabs'
 import * as ProfileCard from '#/components/ProfileCard'
 import {Text} from '#/components/Typography'
 import type * as bsky from '#/types/bsky'
@@ -337,12 +329,13 @@ let Header = ({
           }}
           onEscape={control.close}
         />
-        <Tabs
+        <InterestTabs
           onSelectTab={onSelectTab}
           interests={interests}
           selectedInterest={selectedInterest}
-          hasSearchText={!!searchText}
+          disabled={!!searchText}
           interestsDisplayNames={interestsDisplayNames}
+          TabComponent={Tab}
         />
       </View>
     </View>
@@ -403,99 +396,6 @@ function HeaderTop({guide}: {guide: Follow10ProgressGuide}) {
   )
 }
 
-let Tabs = ({
-  onSelectTab,
-  interests,
-  selectedInterest,
-  hasSearchText,
-  interestsDisplayNames,
-  TabComponent = Tab,
-  contentContainerStyle,
-}: {
-  onSelectTab: (tab: string) => void
-  interests: string[]
-  selectedInterest: string
-  hasSearchText: boolean
-  interestsDisplayNames: Record<string, string>
-  TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
-  contentContainerStyle?: StyleProp<ViewStyle>
-}): React.ReactNode => {
-  const listRef = useRef<ScrollView>(null)
-  const [totalWidth, setTotalWidth] = useState(0)
-  const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
-  const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
-
-  const onInitialLayout = useNonReactiveCallback(() => {
-    const index = interests.indexOf(selectedInterest)
-    scrollIntoViewIfNeeded(index)
-  })
-
-  useEffect(() => {
-    if (tabOffsets) {
-      onInitialLayout()
-    }
-  }, [tabOffsets, onInitialLayout])
-
-  function scrollIntoViewIfNeeded(index: number) {
-    const btnLayout = tabOffsets[index]
-    if (!btnLayout) return
-    listRef.current?.scrollTo({
-      // centered
-      x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
-      animated: true,
-    })
-  }
-
-  function handleSelectTab(index: number) {
-    const tab = interests[index]
-    onSelectTab(tab)
-    scrollIntoViewIfNeeded(index)
-  }
-
-  function handleTabLayout(index: number, x: number, width: number) {
-    if (!tabOffsets.length) {
-      pendingTabOffsets.current[index] = {x, width}
-      if (pendingTabOffsets.current.length === interests.length) {
-        setTabOffsets(pendingTabOffsets.current)
-      }
-    }
-  }
-
-  return (
-    <ScrollView
-      ref={listRef}
-      horizontal
-      contentContainerStyle={[a.gap_sm, a.px_lg, contentContainerStyle]}
-      showsHorizontalScrollIndicator={false}
-      decelerationRate="fast"
-      snapToOffsets={
-        tabOffsets.length === interests.length
-          ? tabOffsets.map(o => o.x - tokens.space.xl)
-          : undefined
-      }
-      onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
-      scrollEventThrottle={200} // big throttle
-    >
-      {interests.map((interest, i) => {
-        const active = interest === selectedInterest && !hasSearchText
-        return (
-          <TabComponent
-            key={interest}
-            onSelectTab={handleSelectTab}
-            active={active}
-            index={i}
-            interest={interest}
-            interestsDisplayName={interestsDisplayNames[interest]}
-            onLayout={handleTabLayout}
-          />
-        )
-      })}
-    </ScrollView>
-  )
-}
-Tabs = memo(Tabs)
-export {Tabs}
-
 let Tab = ({
   onSelectTab,
   interest,
@@ -513,24 +413,36 @@ let Tab = ({
 }): React.ReactNode => {
   const t = useTheme()
   const {_} = useLingui()
-  const activeText = active ? _(msg` (active)`) : ''
+  const label = active
+    ? _(
+        msg({
+          message: `Search for "${interestsDisplayName}" (active)`,
+          comment:
+            'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected.',
+        }),
+      )
+    : _(
+        msg({
+          message: `Search for "${interestsDisplayName}`,
+          comment:
+            'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is not currently active and can be selected.',
+        }),
+      )
   return (
     <View
       key={interest}
       onLayout={e =>
         onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
       }>
-      <Button
-        label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
-        onPress={() => onSelectTab(index)}>
-        {({hovered, pressed, focused}) => (
+      <Button label={label} onPress={() => onSelectTab(index)}>
+        {({hovered, pressed}) => (
           <View
             style={[
               a.rounded_full,
               a.px_lg,
               a.py_sm,
               a.border,
-              active || hovered || pressed || focused
+              active || hovered || pressed
                 ? [
                     t.atoms.bg_contrast_25,
                     {borderColor: t.atoms.bg_contrast_25.backgroundColor},
@@ -540,7 +452,7 @@ let Tab = ({
             <Text
               style={[
                 a.font_medium,
-                active || hovered || pressed || focused
+                active || hovered || pressed
                   ? t.atoms.text
                   : t.atoms.text_contrast_medium,
               ]}>
@@ -759,13 +671,3 @@ function Empty({message}: {message: string}) {
     </View>
   )
 }
-
-export function boostInterests(boosts?: string[]) {
-  return (_a: string, _b: string) => {
-    const indexA = boosts?.indexOf(_a) ?? -1
-    const indexB = boosts?.indexOf(_b) ?? -1
-    const rankA = indexA === -1 ? Infinity : indexA
-    const rankB = indexB === -1 ? Infinity : indexB
-    return rankA - rankB
-  }
-}
diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts
index 05fda9a9f..d4d35ccda 100644
--- a/src/lib/hooks/useDraggableScrollView.ts
+++ b/src/lib/hooks/useDraggableScrollView.ts
@@ -20,9 +20,6 @@ export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
       return
     }
     const slider = ref.current as unknown as HTMLDivElement
-    if (!slider) {
-      return
-    }
     let isDragging = false
     let isMouseDown = false
     let startX = 0
@@ -61,6 +58,9 @@ export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
       e.preventDefault()
       const walk = x - startX
       slider.scrollLeft = scrollLeft - walk
+
+      if (slider.contains(document.activeElement))
+        (document.activeElement as HTMLElement)?.blur?.()
     }
 
     slider.addEventListener('mousedown', mouseDown)
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 114048ab7..391314162 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -9,6 +9,8 @@ export type Gate =
   | 'explore_show_suggested_feeds'
   | 'old_postonboarding'
   | 'onboarding_add_video_feed'
+  | 'onboarding_suggested_accounts'
+  | 'onboarding_value_prop'
   | 'post_follow_profile_suggested_accounts'
   | 'remove_show_latest_button'
   | 'test_gate_1'
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index e51905f84..1cb4eb9d3 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -90,6 +90,13 @@ export type MetricEvents = {
     selectedInterests: string[]
     selectedInterestsLength: number
   }
+  'onboarding:suggestedAccounts:tabPressed': {
+    tab: string
+  }
+  'onboarding:suggestedAccounts:followAllPressed': {
+    tab: string
+    numAccounts: number
+  }
   'onboarding:suggestedAccounts:nextPressed': {
     selectedAccountsLength: number
     skipped: boolean
@@ -118,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
@@ -242,6 +252,7 @@ export type MetricEvents = {
       | 'PostOnboardingFindFollows'
       | 'ImmersiveVideo'
       | 'ExploreSuggestedAccounts'
+      | 'OnboardingSuggestedAccounts'
   }
   'suggestedUser:follow': {
     logContext:
@@ -249,12 +260,17 @@ export type MetricEvents = {
       | 'InterstitialDiscover'
       | 'InterstitialProfile'
       | 'Profile'
+      | 'Onboarding'
     location: 'Card' | 'Profile'
     recId?: number
     position: number
   }
   'suggestedUser:press': {
-    logContext: 'Explore' | 'InterstitialDiscover' | 'InterstitialProfile'
+    logContext:
+      | 'Explore'
+      | 'InterstitialDiscover'
+      | 'InterstitialProfile'
+      | 'Onboarding'
     recId?: number
     position: number
   }
@@ -280,6 +296,7 @@ export type MetricEvents = {
       | 'PostOnboardingFindFollows'
       | 'ImmersiveVideo'
       | 'ExploreSuggestedAccounts'
+      | 'OnboardingSuggestedAccounts'
   }
   'chat:create': {
     logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx
index 16c37358f..6394d9c96 100644
--- a/src/screens/Onboarding/Layout.tsx
+++ b/src/screens/Onboarding/Layout.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useState} from 'react'
 import {ScrollView, View} from 'react-native'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {msg} from '@lingui/macro'
@@ -11,20 +11,23 @@ import {
   atoms as a,
   flatten,
   native,
-  TextStyleProp,
+  type TextStyleProp,
+  tokens,
   useBreakpoints,
   useTheme,
   web,
 } from '#/alf'
 import {leading} from '#/alf/typography'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
+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 COL_WIDTH = 420
+const ONBOARDING_COL_WIDTH = 420
 
 export const OnboardingControls = createPortalGroup()
+export const OnboardingHeaderSlot = createPortalGroup()
 
 export function Layout({children}: React.PropsWithChildren<{}>) {
   const {_} = useLingui()
@@ -46,6 +49,8 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
   const paddingTop = gtMobile ? a.py_5xl : a.py_lg
   const dialogLabel = _(msg`Set up your account`)
 
+  const [footerHeight, setFooterHeight] = useState(0)
+
   return (
     <View
       aria-modal
@@ -62,45 +67,67 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
         t.atoms.bg,
       ]}>
       {__DEV__ && (
-        <View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
-          <Button
-            variant="ghost"
-            color="negative"
-            size="small"
-            onPress={() => onboardDispatch({type: 'skip'})}
-            // DEV ONLY
-            label="Clear onboarding state">
-            <ButtonText>Clear</ButtonText>
-          </Button>
-        </View>
+        <Button
+          variant="ghost"
+          color="negative"
+          size="tiny"
+          onPress={() => onboardDispatch({type: 'skip'})}
+          // DEV ONLY
+          label="Clear onboarding state"
+          style={[
+            a.absolute,
+            a.z_10,
+            {
+              left: '50%',
+              top: insets.top + 2,
+              transform: [{translateX: '-50%'}],
+            },
+          ]}>
+          <ButtonText>[DEV] Clear</ButtonText>
+        </Button>
       )}
 
-      {!gtMobile && state.hasPrev && (
+      {!gtMobile && (
         <View
+          pointerEvents="box-none"
           style={[
             web(a.fixed),
             native(a.absolute),
+            a.left_0,
+            a.right_0,
             a.flex_row,
             a.w_full,
             a.justify_center,
             a.z_20,
             a.px_xl,
-            {
-              top: paddingTop.paddingTop + insets.top - 1,
-            },
+            {top: paddingTop.paddingTop + insets.top - 1},
           ]}>
-          <View style={[a.w_full, a.align_start, {maxWidth: COL_WIDTH}]}>
-            <Button
-              key={state.activeStep} // remove focus state on nav
-              variant="ghost"
-              color="secondary"
-              size="small"
-              shape="round"
-              label={_(msg`Go back to previous step`)}
-              style={[a.absolute]}
-              onPress={() => dispatch({type: 'prev'})}>
-              <ButtonIcon icon={ChevronLeft} />
-            </Button>
+          <View
+            pointerEvents="box-none"
+            style={[
+              a.w_full,
+              a.align_start,
+              a.flex_row,
+              a.justify_between,
+              {maxWidth: ONBOARDING_COL_WIDTH},
+            ]}>
+            {state.hasPrev ? (
+              <Button
+                key={state.activeStep} // remove focus state on nav
+                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>
       )}
@@ -109,22 +136,24 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
         ref={scrollview}
         style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
         contentContainerStyle={{borderWidth: 0}}
-        // @ts-ignore web only --prf
+        scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
+        // @ts-expect-error web only --prf
         dataSet={{'stable-gutters': 1}}>
         <View
           style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
-          <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
+          <View style={[a.flex_1, {maxWidth: ONBOARDING_COL_WIDTH}]}>
             <View style={[a.w_full, a.align_center, paddingTop]}>
               <View
                 style={[
                   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)
-                  .map((_, i) => (
+                  .map((__, i) => (
                     <View
                       key={i}
                       style={[
@@ -144,19 +173,16 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
               </View>
             </View>
 
-            <View
-              style={[a.w_full, a.mb_5xl, {paddingTop: gtMobile ? 20 : 40}]}>
-              {children}
-            </View>
+            <View style={[a.w_full, a.mb_5xl, a.pt_md]}>{children}</View>
 
-            <View style={{height: 400}} />
+            <View style={{height: 100 + footerHeight}} />
           </View>
         </View>
       </ScrollView>
 
       <View
+        onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}
         style={[
-          // @ts-ignore web only -prf
           isWeb ? a.fixed : a.absolute,
           {bottom: 0, left: 0, right: 0},
           t.atoms.bg,
@@ -167,30 +193,30 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
           isWeb
             ? a.py_2xl
             : {
-                paddingTop: a.pt_lg.paddingTop,
-                paddingBottom: insets.bottom + 10,
+                paddingTop: tokens.space.md,
+                paddingBottom: insets.bottom + tokens.space.md,
               },
         ]}>
         <View
           style={[
             a.w_full,
-            {maxWidth: COL_WIDTH},
-            gtMobile && [a.flex_row, a.justify_between],
+            {maxWidth: ONBOARDING_COL_WIDTH},
+            gtMobile && [a.flex_row, a.justify_between, a.align_center],
           ]}>
           {gtMobile &&
             (state.hasPrev ? (
               <Button
                 key={state.activeStep} // remove focus state on nav
-                variant="solid"
                 color="secondary"
-                size="large"
-                shape="round"
+                variant="ghost"
+                shape="square"
+                size="small"
                 label={_(msg`Go back to previous step`)}
                 onPress={() => dispatch({type: 'prev'})}>
-                <ButtonIcon icon={ChevronLeft} />
+                <ButtonIcon icon={ArrowLeft} size="lg" />
               </Button>
             ) : (
-              <View style={{height: 54}} />
+              <View style={{height: 33}} />
             ))}
           <OnboardingControls.Outlet />
         </View>
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index 54d282a5e..f8040f3a5 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,13 +44,22 @@ 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 {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
 import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2'
@@ -53,10 +70,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 +82,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 +261,267 @@ export function StepFinished() {
     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>
+              ) : subStep === 2 ? (
+                <Trans>Let's go!</Trans>
+              ) : (
+                <Trans>Next</Trans>
+              )}
+            </ButtonText>
+            {subStep === 2 && (
+              <ButtonIcon icon={saving ? Loader : ArrowRight} />
+            )}
+          </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 (
     <View style={[a.align_start]}>
       <IconCircle icon={Check} style={[a.mb_2xl]} />
@@ -305,7 +582,6 @@ export function StepFinished() {
         <Button
           disabled={saving}
           key={state.activeStep} // remove focus state on nav
-          variant="solid"
           color="primary"
           size="large"
           label={_(msg`Complete onboarding and start using your account`)}
diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx
index 2a121cac6..3bde22136 100644
--- a/src/screens/Onboarding/StepInterests/index.tsx
+++ b/src/screens/Onboarding/StepInterests/index.tsx
@@ -160,7 +160,16 @@ export function StepInterests() {
 
       <View style={[a.w_full, a.pt_2xl]}>
         {isLoading ? (
-          <Loader size="xl" />
+          <View
+            style={[
+              a.flex_1,
+              a.mt_md,
+              a.align_center,
+              a.justify_center,
+              {minHeight: 400},
+            ]}>
+            <Loader size="xl" />
+          </View>
         ) : isError || !data ? (
           <View
             style={[
diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx
index 30da5cbb5..fd5f9b6fb 100644
--- a/src/screens/Onboarding/StepProfile/index.tsx
+++ b/src/screens/Onboarding/StepProfile/index.tsx
@@ -266,7 +266,7 @@ export function StepProfile() {
         </View>
 
         <OnboardingControls.Portal>
-          <View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}>
+          <View style={[a.gap_md, gtMobile && a.flex_row_reverse]}>
             <Button
               variant="solid"
               color="primary"
diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
new file mode 100644
index 000000000..5a9d3464c
--- /dev/null
+++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
@@ -0,0 +1,356 @@
+import {useCallback, useContext, useMemo, useState} from 'react'
+import {View} from 'react-native'
+import {type ModerationOpts} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+import * as bcp47Match from 'bcp-47-match'
+
+import {wait} from '#/lib/async/wait'
+import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {updateProfileShadow} from '#/state/cache/profile-shadow'
+import {useLanguagePrefs} from '#/state/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useAgent, useSession} from '#/state/session'
+import {useOnboardingDispatch} from '#/state/shell'
+import {OnboardingControls} from '#/screens/Onboarding/Layout'
+import {
+  Context,
+  popularInterests,
+  useInterestsDisplayNames,
+} from '#/screens/Onboarding/state'
+import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
+import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
+import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
+import {boostInterests, InterestTabs} from '#/components/InterestTabs'
+import {Loader} from '#/components/Loader'
+import * as ProfileCard from '#/components/ProfileCard'
+import * as toast from '#/components/Toast'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
+import {bulkWriteFollows} from '../util'
+
+export function StepSuggestedAccounts() {
+  const {_} = useLingui()
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  const moderationOpts = useModerationOpts()
+  const agent = useAgent()
+  const {currentAccount} = useSession()
+  const queryClient = useQueryClient()
+
+  const {state, dispatch} = useContext(Context)
+  const onboardDispatch = useOnboardingDispatch()
+
+  const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
+  // keeping track of who was followed via the follow all button
+  // so we can enable/disable the button without having to dig through the shadow cache
+  const [followedUsers, setFollowedUsers] = useState<string[]>([])
+
+  /*
+   * Special language handling copied wholesale from the Explore screen
+   */
+  const {contentLanguages} = useLanguagePrefs()
+  const useFullExperience = useMemo(() => {
+    if (contentLanguages.length === 0) return true
+    return bcp47Match.basicFilter('en', contentLanguages).length > 0
+  }, [contentLanguages])
+  const interestsDisplayNames = useInterestsDisplayNames()
+  const interests = Object.keys(interestsDisplayNames)
+    .sort(boostInterests(popularInterests))
+    .sort(boostInterests(state.interestsStepResults.selectedInterests))
+  const {
+    data: suggestedUsers,
+    isLoading,
+    error,
+    isRefetching,
+    refetch,
+  } = useSuggestedUsers({
+    category: selectedInterest || (useFullExperience ? null : interests[0]),
+    search: !useFullExperience,
+    overrideInterests: state.interestsStepResults.selectedInterests,
+  })
+
+  const isError = !!error
+
+  const skipOnboarding = useCallback(() => {
+    onboardDispatch({type: 'finish'})
+    dispatch({type: 'finish'})
+  }, [onboardDispatch, dispatch])
+
+  const followableDids =
+    suggestedUsers?.actors
+      .filter(
+        user =>
+          user.did !== currentAccount?.did &&
+          !isBlockedOrBlocking(user) &&
+          !isMuted(user) &&
+          !user.viewer?.following &&
+          !followedUsers.includes(user.did),
+      )
+      .map(user => user.did) ?? []
+
+  const {mutate: followAll, isPending: isFollowingAll} = useMutation({
+    onMutate: () => {
+      logger.metric('onboarding:suggestedAccounts:followAllPressed', {
+        tab: selectedInterest ?? 'all',
+        numAccounts: followableDids.length,
+      })
+    },
+    mutationFn: async () => {
+      for (const did of followableDids) {
+        updateProfileShadow(queryClient, did, {
+          followingUri: 'pending',
+        })
+      }
+      const uris = await wait(1e3, bulkWriteFollows(agent, followableDids))
+      for (const did of followableDids) {
+        const uri = uris.get(did)
+        updateProfileShadow(queryClient, did, {
+          followingUri: uri,
+        })
+      }
+      return followableDids
+    },
+    onSuccess: newlyFollowed => {
+      toast.show(_(msg`Followed all accounts!`), {type: 'success'})
+      setFollowedUsers(followed => [...followed, ...newlyFollowed])
+    },
+    onError: () => {
+      toast.show(
+        _(msg`Failed to follow all suggested accounts, please try again`),
+        {type: 'error'},
+      )
+    },
+  })
+
+  const canFollowAll = followableDids.length > 0 && !isFollowingAll
+
+  return (
+    <View style={[a.align_start]} testID="onboardingInterests">
+      <Text style={[a.font_heavy, a.text_3xl]}>
+        <Trans comment="Accounts suggested to the user for them to follow">
+          Suggested for you
+        </Trans>
+      </Text>
+
+      <View
+        style={[
+          a.overflow_hidden,
+          a.mt_lg,
+          isWeb ? a.max_w_full : {marginHorizontal: tokens.space.xl * -1},
+          a.flex_1,
+          a.justify_start,
+        ]}>
+        <TabBar
+          selectedInterest={selectedInterest}
+          onSelectInterest={setSelectedInterest}
+          defaultTabLabel={_(
+            msg({
+              message: 'All',
+              comment: 'the default tab in the interests tab bar',
+            }),
+          )}
+          selectedInterests={state.interestsStepResults.selectedInterests}
+        />
+
+        {isLoading || !moderationOpts ? (
+          <View
+            style={[
+              a.flex_1,
+              a.mt_md,
+              a.align_center,
+              a.justify_center,
+              {minHeight: 400},
+            ]}>
+            <Loader size="xl" />
+          </View>
+        ) : isError ? (
+          <View style={[a.flex_1, a.px_xl, a.pt_5xl]}>
+            <Admonition type="error">
+              <Trans>
+                An error occurred while fetching suggested accounts.
+              </Trans>
+            </Admonition>
+          </View>
+        ) : (
+          <View
+            style={[
+              a.flex_1,
+              a.mt_md,
+              a.border_y,
+              t.atoms.border_contrast_low,
+              isWeb && [a.border_x, a.rounded_sm, a.overflow_hidden],
+            ]}>
+            {suggestedUsers?.actors.map((user, index) => (
+              <SuggestedProfileCard
+                key={user.did}
+                profile={user}
+                moderationOpts={moderationOpts}
+                position={index}
+              />
+            ))}
+          </View>
+        )}
+      </View>
+
+      <OnboardingControls.Portal>
+        {isError ? (
+          <View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
+            <Button
+              disabled={isRefetching}
+              color="secondary"
+              size="large"
+              label={_(msg`Retry`)}
+              onPress={() => refetch()}>
+              <ButtonText>
+                <Trans>Retry</Trans>
+              </ButtonText>
+              <ButtonIcon icon={ArrowRotateCounterClockwiseIcon} />
+            </Button>
+            <Button
+              color="secondary"
+              size="large"
+              label={_(msg`Skip this flow`)}
+              onPress={skipOnboarding}>
+              <ButtonText>
+                <Trans>Skip</Trans>
+              </ButtonText>
+            </Button>
+          </View>
+        ) : (
+          <View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
+            <Button
+              disabled={!canFollowAll}
+              color="secondary"
+              size="large"
+              label={_(msg`Follow all accounts`)}
+              onPress={() => followAll()}>
+              <ButtonText>
+                <Trans>Follow all</Trans>
+              </ButtonText>
+              <ButtonIcon icon={isFollowingAll ? Loader : PlusIcon} />
+            </Button>
+            <Button
+              disabled={isFollowingAll}
+              color="primary"
+              size="large"
+              label={_(msg`Continue to next step`)}
+              onPress={() => dispatch({type: 'next'})}>
+              <ButtonText>
+                <Trans>Continue</Trans>
+              </ButtonText>
+            </Button>
+          </View>
+        )}
+      </OnboardingControls.Portal>
+    </View>
+  )
+}
+
+function TabBar({
+  selectedInterest,
+  onSelectInterest,
+  selectedInterests,
+  hideDefaultTab,
+  defaultTabLabel,
+}: {
+  selectedInterest: string | null
+  onSelectInterest: (interest: string | null) => void
+  selectedInterests: string[]
+  hideDefaultTab?: boolean
+  defaultTabLabel?: string
+}) {
+  const {_} = useLingui()
+  const interestsDisplayNames = useInterestsDisplayNames()
+  const interests = Object.keys(interestsDisplayNames)
+    .sort(boostInterests(popularInterests))
+    .sort(boostInterests(selectedInterests))
+
+  return (
+    <InterestTabs
+      interests={hideDefaultTab ? interests : ['all', ...interests]}
+      selectedInterest={
+        selectedInterest || (hideDefaultTab ? interests[0] : 'all')
+      }
+      onSelectTab={tab => {
+        logger.metric(
+          'onboarding:suggestedAccounts:tabPressed',
+          {tab: tab},
+          {statsig: true},
+        )
+        onSelectInterest(tab === 'all' ? null : tab)
+      }}
+      interestsDisplayNames={
+        hideDefaultTab
+          ? interestsDisplayNames
+          : {
+              all: defaultTabLabel || _(msg`For You`),
+              ...interestsDisplayNames,
+            }
+      }
+      gutterWidth={isWeb ? 0 : tokens.space.xl}
+    />
+  )
+}
+
+function SuggestedProfileCard({
+  profile,
+  moderationOpts,
+  position,
+}: {
+  profile: bsky.profile.AnyProfileView
+  moderationOpts: ModerationOpts
+  position: number
+}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.flex_1,
+        a.w_full,
+        a.py_lg,
+        a.px_xl,
+        position !== 0 && a.border_t,
+        t.atoms.border_contrast_low,
+      ]}>
+      <ProfileCard.Outer>
+        <ProfileCard.Header>
+          <ProfileCard.Avatar
+            profile={profile}
+            moderationOpts={moderationOpts}
+            disabledPreview
+          />
+          <ProfileCard.NameAndHandle
+            profile={profile}
+            moderationOpts={moderationOpts}
+          />
+          <ProfileCard.FollowButton
+            profile={profile}
+            moderationOpts={moderationOpts}
+            withIcon={false}
+            logContext="OnboardingSuggestedAccounts"
+            onFollow={() => {
+              logger.metric(
+                'suggestedUser:follow',
+                {
+                  logContext: 'Onboarding',
+                  location: 'Card',
+                  recId: undefined,
+                  position,
+                },
+                {statsig: true},
+              )
+            }}
+          />
+        </ProfileCard.Header>
+        <ProfileCard.Description profile={profile} numberOfLines={3} />
+      </ProfileCard.Outer>
+    </View>
+  )
+}
diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx
index a5c423ca1..2291e5e4f 100644
--- a/src/screens/Onboarding/index.tsx
+++ b/src/screens/Onboarding/index.tsx
@@ -1,21 +1,35 @@
-import React from 'react'
+import {useMemo, useReducer} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
+import {useGate} from '#/lib/statsig/statsig'
+import {
+  Layout,
+  OnboardingControls,
+  OnboardingHeaderSlot,
+} from '#/screens/Onboarding/Layout'
 import {Context, initialState, reducer} from '#/screens/Onboarding/state'
 import {StepFinished} from '#/screens/Onboarding/StepFinished'
 import {StepInterests} from '#/screens/Onboarding/StepInterests'
 import {StepProfile} from '#/screens/Onboarding/StepProfile'
 import {Portal} from '#/components/Portal'
+import {StepSuggestedAccounts} from './StepSuggestedAccounts'
 
 export function Onboarding() {
   const {_} = useLingui()
-  const [state, dispatch] = React.useReducer(reducer, {
+  const gate = useGate()
+  const showValueProp = gate('onboarding_value_prop')
+  const showSuggestedAccounts = gate('onboarding_suggested_accounts')
+  const [state, dispatch] = useReducer(reducer, {
     ...initialState,
+    totalSteps: showSuggestedAccounts ? 4 : 3,
+    experiments: {
+      onboarding_suggested_accounts: showSuggestedAccounts,
+      onboarding_value_prop: showValueProp,
+    },
   })
 
-  const interestsDisplayNames = React.useMemo(() => {
+  const interestsDisplayNames = useMemo(() => {
     return {
       news: _(msg`News`),
       journalism: _(msg`Journalism`),
@@ -45,17 +59,22 @@ export function Onboarding() {
   return (
     <Portal>
       <OnboardingControls.Provider>
-        <Context.Provider
-          value={React.useMemo(
-            () => ({state, dispatch, interestsDisplayNames}),
-            [state, dispatch, interestsDisplayNames],
-          )}>
-          <Layout>
-            {state.activeStep === 'profile' && <StepProfile />}
-            {state.activeStep === 'interests' && <StepInterests />}
-            {state.activeStep === 'finished' && <StepFinished />}
-          </Layout>
-        </Context.Provider>
+        <OnboardingHeaderSlot.Provider>
+          <Context.Provider
+            value={useMemo(
+              () => ({state, dispatch, interestsDisplayNames}),
+              [state, dispatch, interestsDisplayNames],
+            )}>
+            <Layout>
+              {state.activeStep === 'profile' && <StepProfile />}
+              {state.activeStep === 'interests' && <StepInterests />}
+              {state.activeStep === 'suggested-accounts' && (
+                <StepSuggestedAccounts />
+              )}
+              {state.activeStep === 'finished' && <StepFinished />}
+            </Layout>
+          </Context.Provider>
+        </OnboardingHeaderSlot.Provider>
       </OnboardingControls.Provider>
     </Portal>
   )
diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts
index cbb466245..31f6eb039 100644
--- a/src/screens/Onboarding/state.ts
+++ b/src/screens/Onboarding/state.ts
@@ -11,7 +11,7 @@ import {
 export type OnboardingState = {
   hasPrev: boolean
   totalSteps: number
-  activeStep: 'profile' | 'interests' | 'finished'
+  activeStep: 'profile' | 'interests' | 'suggested-accounts' | 'finished'
   activeStepIndex: number
 
   interestsStepResults: {
@@ -34,6 +34,11 @@ export type OnboardingState = {
       backgroundColor: AvatarColor
     }
   }
+
+  experiments?: {
+    onboarding_suggested_accounts?: boolean
+    onboarding_value_prop?: boolean
+  }
 }
 
 export type OnboardingAction =
@@ -160,22 +165,49 @@ export function reducer(
 
   switch (a.type) {
     case 'next': {
-      if (s.activeStep === 'profile') {
-        next.activeStep = 'interests'
-        next.activeStepIndex = 2
-      } else if (s.activeStep === 'interests') {
-        next.activeStep = 'finished'
-        next.activeStepIndex = 3
+      if (s.experiments?.onboarding_suggested_accounts) {
+        if (s.activeStep === 'profile') {
+          next.activeStep = 'interests'
+          next.activeStepIndex = 2
+        } else if (s.activeStep === 'interests') {
+          next.activeStep = 'suggested-accounts'
+          next.activeStepIndex = 3
+        }
+        if (s.activeStep === 'suggested-accounts') {
+          next.activeStep = 'finished'
+          next.activeStepIndex = 4
+        }
+      } else {
+        if (s.activeStep === 'profile') {
+          next.activeStep = 'interests'
+          next.activeStepIndex = 2
+        } else if (s.activeStep === 'interests') {
+          next.activeStep = 'finished'
+          next.activeStepIndex = 3
+        }
       }
       break
     }
     case 'prev': {
-      if (s.activeStep === 'interests') {
-        next.activeStep = 'profile'
-        next.activeStepIndex = 1
-      } else if (s.activeStep === 'finished') {
-        next.activeStep = 'interests'
-        next.activeStepIndex = 2
+      if (s.experiments?.onboarding_suggested_accounts) {
+        if (s.activeStep === 'interests') {
+          next.activeStep = 'profile'
+          next.activeStepIndex = 1
+        } else if (s.activeStep === 'suggested-accounts') {
+          next.activeStep = 'interests'
+          next.activeStepIndex = 2
+        } else if (s.activeStep === 'finished') {
+          next.activeStep = 'suggested-accounts'
+          next.activeStepIndex = 3
+        }
+      } else {
+        if (s.activeStep === 'interests') {
+          next.activeStep = 'profile'
+          next.activeStepIndex = 1
+        } else if (s.activeStep === 'finished') {
+          next.activeStep = 'interests'
+          next.activeStepIndex = 2
+        }
       }
       break
     }
diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts
index d14c9562e..b08f0408e 100644
--- a/src/screens/Onboarding/util.ts
+++ b/src/screens/Onboarding/util.ts
@@ -1,9 +1,9 @@
 import {
-  $Typed,
-  AppBskyGraphFollow,
-  AppBskyGraphGetFollows,
-  BskyAgent,
-  ComAtprotoRepoApplyWrites,
+  type $Typed,
+  type AppBskyGraphFollow,
+  type AppBskyGraphGetFollows,
+  type BskyAgent,
+  type ComAtprotoRepoApplyWrites,
 } from '@atproto/api'
 import {TID} from '@atproto/common-web'
 import chunk from 'lodash.chunk'
@@ -42,10 +42,10 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
   }
   await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length)
 
-  const followUris = new Map()
+  const followUris = new Map<string, string>()
   for (const r of followWrites) {
     followUris.set(
-      r.value.subject,
+      r.value.subject as string,
       `at://${session.did}/app.bsky.graph.follow/${r.rkey}`,
     )
   }
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
index baf69cd7f..cefe68b01 100644
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -66,9 +66,9 @@ import {
 import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
 import {StarterPack} from '#/components/icons/StarterPack'
 import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
+import {boostInterests} from '#/components/InterestTabs'
 import {Loader} from '#/components/Loader'
 import * as ProfileCard from '#/components/ProfileCard'
-import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
 import {SubtleHover} from '#/components/SubtleHover'
 import {Text} from '#/components/Typography'
 import * as ModuleHeader from './components/ModuleHeader'
diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
index fd37544f4..71bfd6547 100644
--- a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
+++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
@@ -14,11 +14,9 @@ import {
 } from '#/screens/Onboarding/state'
 import {useTheme} from '#/alf'
 import {atoms as a} from '#/alf'
-import {Button} from '#/components/Button'
+import {boostInterests, InterestTabs} from '#/components/InterestTabs'
 import * as ProfileCard from '#/components/ProfileCard'
-import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
 import {SubtleHover} from '#/components/SubtleHover'
-import {Text} from '#/components/Typography'
 import type * as bsky from '#/types/bsky'
 
 export function useLoadEnoughProfiles({
@@ -59,10 +57,12 @@ export function SuggestedAccountsTabBar({
   selectedInterest,
   onSelectInterest,
   hideDefaultTab,
+  defaultTabLabel,
 }: {
   selectedInterest: string | null
   onSelectInterest: (interest: string | null) => void
   hideDefaultTab?: boolean
+  defaultTabLabel?: string
 }) {
   const {_} = useLingui()
   const interestsDisplayNames = useInterestsDisplayNames()
@@ -71,9 +71,10 @@ export function SuggestedAccountsTabBar({
   const interests = Object.keys(interestsDisplayNames)
     .sort(boostInterests(popularInterests))
     .sort(boostInterests(personalizedInterests))
+
   return (
     <BlockDrawerGesture>
-      <Tabs
+      <InterestTabs
         interests={hideDefaultTab ? interests : ['all', ...interests]}
         selectedInterest={
           selectedInterest || (hideDefaultTab ? interests[0] : 'all')
@@ -86,82 +87,19 @@ export function SuggestedAccountsTabBar({
           )
           onSelectInterest(tab === 'all' ? null : tab)
         }}
-        hasSearchText={false}
         interestsDisplayNames={
           hideDefaultTab
             ? interestsDisplayNames
             : {
-                all: _(msg`For You`),
+                all: defaultTabLabel || _(msg`For You`),
                 ...interestsDisplayNames,
               }
         }
-        TabComponent={Tab}
-        contentContainerStyle={[
-          {
-            // visual alignment
-            paddingLeft: a.px_md.paddingLeft,
-          },
-        ]}
       />
     </BlockDrawerGesture>
   )
 }
 
-let Tab = ({
-  onSelectTab,
-  interest,
-  active,
-  index,
-  interestsDisplayName,
-  onLayout,
-}: {
-  onSelectTab: (index: number) => void
-  interest: string
-  active: boolean
-  index: number
-  interestsDisplayName: string
-  onLayout: (index: number, x: number, width: number) => void
-}): React.ReactNode => {
-  const t = useTheme()
-  const {_} = useLingui()
-  const activeText = active ? _(msg` (active)`) : ''
-  return (
-    <View
-      key={interest}
-      onLayout={e =>
-        onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
-      }>
-      <Button
-        label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
-        onPress={() => onSelectTab(index)}>
-        {({hovered, pressed, focused}) => (
-          <View
-            style={[
-              a.rounded_full,
-              a.px_lg,
-              a.py_sm,
-              a.border,
-              active || hovered || pressed || focused
-                ? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
-                : [t.atoms.bg, t.atoms.border_contrast_low],
-            ]}>
-            <Text
-              style={[
-                a.font_medium,
-                active || hovered || pressed || focused
-                  ? t.atoms.text
-                  : t.atoms.text_contrast_medium,
-              ]}>
-              {interestsDisplayName}
-            </Text>
-          </View>
-        )}
-      </Button>
-    </View>
-  )
-}
-Tab = memo(Tab)
-
 /**
  * Profile card for suggested accounts. Note: border is on the bottom edge
  */
diff --git a/src/screens/Search/util/useSuggestedUsers.ts b/src/screens/Search/util/useSuggestedUsers.ts
index aa29dad8c..9ca2c558a 100644
--- a/src/screens/Search/util/useSuggestedUsers.ts
+++ b/src/screens/Search/util/useSuggestedUsers.ts
@@ -11,6 +11,7 @@ import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
 export function useSuggestedUsers({
   category = null,
   search = false,
+  overrideInterests,
 }: {
   category?: string | null
   /**
@@ -18,11 +19,17 @@ export function useSuggestedUsers({
    * based on the user's "app language setting
    */
   search?: boolean
+  /**
+   * In onboarding, interests haven't been saved to prefs yet, so we need to
+   * pass them down through here
+   */
+  overrideInterests?: string[]
 }) {
   const interestsDisplayNames = useInterestsDisplayNames()
   const curated = useGetSuggestedUsersQuery({
     enabled: !search,
     category,
+    overrideInterests,
   })
   const searched = useActorSearchPaginated({
     enabled: !!search,
@@ -43,6 +50,7 @@ export function useSuggestedUsers({
         isLoading: searched.isLoading,
         error: searched.error,
         isRefetching: searched.isRefetching,
+        refetch: searched.refetch,
       }
     } else {
       return {
@@ -50,6 +58,7 @@ export function useSuggestedUsers({
         isLoading: curated.isLoading,
         error: curated.error,
         isRefetching: curated.isRefetching,
+        refetch: curated.refetch,
       }
     }
   }, [curated, searched, search])
diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
index 05cc4d74d..898029398 100644
--- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
@@ -17,6 +17,7 @@ export type QueryProps = {
   category?: string | null
   limit?: number
   enabled?: boolean
+  overrideInterests?: string[]
 }
 
 export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
@@ -24,6 +25,7 @@ export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
   getSuggestedUsersQueryKeyRoot,
   props.category,
   props.limit,
+  props.overrideInterests?.join(','),
 ]
 
 export function useGetSuggestedUsersQuery(props: QueryProps) {
@@ -36,6 +38,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
     queryKey: createGetSuggestedUsersQueryKey(props),
     queryFn: async () => {
       const contentLangs = getContentLanguages().join(',')
+      const interests = aggregateUserInterests(preferences)
       const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
         {
           category: props.category ?? undefined,
@@ -43,7 +46,11 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
         },
         {
           headers: {
-            ...createBskyTopicsHeader(aggregateUserInterests(preferences)),
+            ...createBskyTopicsHeader(
+              props.overrideInterests && props.overrideInterests.length > 0
+                ? props.overrideInterests.join(',')
+                : interests,
+            ),
             'Accept-Language': contentLangs,
           },
         },