diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-08-27 14:17:45 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-08-27 04:17:45 -0700 |
commit | eac02901435d7bc79a28e0bff665352b814f9508 (patch) | |
tree | 8d770830a0c7081c5e4ed941b192da34ecb538a8 /src/screens/Onboarding/StepFinished.tsx | |
parent | 0617fca5ef1d30e7db49eb7dc8e66f6b586cc207 (diff) | |
download | voidsky-eac02901435d7bc79a28e0bff665352b814f9508.tar.zst |
Add suggested follows experiment to onboarding (#8847)
* add new gated screen to onboarding * add tab bar, adjust layout * replace chevron with arrow * get suggested accounts working on native * tweaks for web * add metrics to follow all * rm non-functional link from card * ensure selected interests are passed through to interests query * fix logcontext * followed all accounts! toast * rm save interests function * Update src/screens/Onboarding/StepSuggestedAccounts/index.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * use admonition * rm comment * Better interest tabs (#8879) * make tabs draggable * move tab component to own file * rm focused state from tab, improve label * add focus styles, remove focus when dragging * rm log * add arrows to tabs * rename Tabs -> InterestTabs * try and simplify approach * rename ref * Update InterestTabs.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/InterestTabs.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/ProgressGuide/FollowDialog.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/ProgressGuide/FollowDialog.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add newline --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * fix flex problem * Add value proposition screen experiment (#8898) * add assets * add value prop experiment * add alt text * add metrics * add transitions * add skip button * tweak copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add borderless variant for web * rm pointer events --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Add slight delay, prevent layout shift * Handle layout shift, add Let's Go! text --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/screens/Onboarding/StepFinished.tsx')
-rw-r--r-- | src/screens/Onboarding/StepFinished.tsx | 292 |
1 files changed, 284 insertions, 8 deletions
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`)} |