refreshed onboarding styles, rm stepper

This commit is contained in:
Samuel Newman
2025-12-05 21:12:16 +02:00
committed by Eric Bailey
parent da55868ddc
commit f8e94563cd
12 changed files with 180 additions and 163 deletions
+1
View File
@@ -129,6 +129,7 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
size="small" size="small"
variant="ghost" variant="ghost"
color="secondary" color="secondary"
shape="round"
onPress={onPressBack} onPress={onPressBack}
hitSlop={HITSLOP_30} hitSlop={HITSLOP_30}
style={[ style={[
+2 -2
View File
@@ -23,7 +23,7 @@ export function FindContactsFlow({
<PhoneInput <PhoneInput
state={state} state={state}
dispatch={dispatch} dispatch={dispatch}
showSkipButton={context === 'Onboarding'} context={context}
onSkip={onCancel} onSkip={onCancel}
/> />
)} )}
@@ -31,7 +31,7 @@ export function FindContactsFlow({
<VerifyNumber <VerifyNumber
state={state} state={state}
dispatch={dispatch} dispatch={dispatch}
showSkipButton={context === 'Onboarding'} context={context}
onSkip={onCancel} onSkip={onCancel}
/> />
)} )}
@@ -21,6 +21,7 @@ import {
useGutters, useGutters,
useTheme, useTheme,
} from '#/alf' } from '#/alf'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect' import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect'
@@ -40,12 +41,12 @@ import {type Action, type State, useOnPressBackButton} from '../state'
export function PhoneInput({ export function PhoneInput({
state, state,
dispatch, dispatch,
showSkipButton, context,
onSkip, onSkip,
}: { }: {
state: Extract<State, {step: '1: phone input'}> state: Extract<State, {step: '1: phone input'}>
dispatch: React.ActionDispatch<[Action]> dispatch: React.ActionDispatch<[Action]>
showSkipButton: boolean context: 'Onboarding' | 'Standalone'
onSkip: () => void onSkip: () => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
@@ -131,7 +132,7 @@ export function PhoneInput({
<Layout.Header.Outer noBottomBorder> <Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton onPress={onPressBack} /> <Layout.Header.BackButton onPress={onPressBack} />
<Layout.Header.Content /> <Layout.Header.Content />
{showSkipButton ? ( {context === 'Onboarding' ? (
<Button <Button
size="small" size="small"
color="secondary" color="secondary"
@@ -149,6 +150,7 @@ export function PhoneInput({
<Layout.Content <Layout.Content
contentContainerStyle={[gutters, a.pt_sm, a.flex_1]} contentContainerStyle={[gutters, a.pt_sm, a.flex_1]}
keyboardShouldPersistTaps="handled"> keyboardShouldPersistTaps="handled">
{context === 'Onboarding' && <OnboardingPosition />}
<Text style={[a.font_bold, a.text_3xl]}> <Text style={[a.font_bold, a.text_3xl]}>
<Trans>Verify phone number</Trans> <Trans>Verify phone number</Trans>
</Text> </Text>
@@ -8,6 +8,7 @@ import {clamp} from '#/lib/numbers'
import {cleanError, isNetworkError} from '#/lib/strings/errors' import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {atoms as a, useGutters, useTheme} from '#/alf' import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotateCounterClockwise' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotateCounterClockwise'
@@ -25,12 +26,12 @@ import {type Action, type State, useOnPressBackButton} from '../state'
export function VerifyNumber({ export function VerifyNumber({
state, state,
dispatch, dispatch,
showSkipButton, context,
onSkip, onSkip,
}: { }: {
state: Extract<State, {step: '2: verify number'}> state: Extract<State, {step: '2: verify number'}>
dispatch: React.ActionDispatch<[Action]> dispatch: React.ActionDispatch<[Action]>
showSkipButton: boolean context: 'Onboarding' | 'Standalone'
onSkip: () => void onSkip: () => void
}) { }) {
const t = useTheme() const t = useTheme()
@@ -139,7 +140,7 @@ export function VerifyNumber({
<Layout.Header.Outer noBottomBorder> <Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton onPress={onPressBack} /> <Layout.Header.BackButton onPress={onPressBack} />
<Layout.Header.Content /> <Layout.Header.Content />
{showSkipButton ? ( {context === 'Onboarding' ? (
<Button <Button
size="small" size="small"
color="secondary" color="secondary"
@@ -157,6 +158,7 @@ export function VerifyNumber({
<Layout.Content <Layout.Content
contentContainerStyle={[gutters, a.pt_sm, a.flex_1]} contentContainerStyle={[gutters, a.pt_sm, a.flex_1]}
keyboardShouldPersistTaps="always"> keyboardShouldPersistTaps="always">
{context === 'Onboarding' && <OnboardingPosition />}
<Text style={[a.font_bold, a.text_3xl]}> <Text style={[a.font_bold, a.text_3xl]}>
<Trans>Verify phone number</Trans> <Trans>Verify phone number</Trans>
</Text> </Text>
@@ -18,6 +18,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {List, type ListMethods} from '#/view/com/util/List' import {List, type ListMethods} from '#/view/com/util/List'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {bulkWriteFollows} from '#/screens/Onboarding/util' import {bulkWriteFollows} from '#/screens/Onboarding/util'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf' import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -323,7 +324,13 @@ export function ViewMatches({
</Layout.Header.Outer> </Layout.Header.Outer>
)} )}
{!isTotallyEmpty && ( {!isTotallyEmpty && (
<View style={[gutter, a.mb_md, context === 'Onboarding' && a.mt_sm]}> <View
style={[
gutter,
a.mb_md,
context === 'Onboarding' && [a.mt_sm, a.gap_sm],
]}>
{context === 'Onboarding' && <OnboardingPosition />}
<SearchInput <SearchInput
placeholder={_(msg`Search contacts`)} placeholder={_(msg`Search contacts`)}
value={search} value={search}
+73 -83
View File
@@ -1,8 +1,7 @@
import React, {useState} from 'react' import {useEffect, useRef, useState} from 'react'
import {ScrollView, View} from 'react-native' import {ScrollView, View} from 'react-native'
import {Dimensions} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isAndroid, isWeb} from '#/platform/detection' import {isAndroid, isWeb} from '#/platform/detection'
@@ -22,6 +21,7 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons
import {HEADER_SLOT_SIZE} from '#/components/Layout' import {HEADER_SLOT_SIZE} from '#/components/Layout'
import {createPortalGroup} from '#/components/Portal' import {createPortalGroup} from '#/components/Portal'
import {P, Text} from '#/components/Typography' import {P, Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
const ONBOARDING_COL_WIDTH = 420 const ONBOARDING_COL_WIDTH = 420
@@ -35,10 +35,10 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const onboardDispatch = useOnboardingDispatch() const onboardDispatch = useOnboardingDispatch()
const {state, dispatch} = useOnboardingInternalState() const {state, dispatch} = useOnboardingInternalState()
const scrollview = React.useRef<ScrollView>(null) const scrollview = useRef<ScrollView>(null)
const prevActiveStep = React.useRef<string>(state.activeStep) const prevActiveStep = useRef<string>(state.activeStep)
React.useEffect(() => { useEffect(() => {
if (state.activeStep !== prevActiveStep.current) { if (state.activeStep !== prevActiveStep.current) {
prevActiveStep.current = state.activeStep prevActiveStep.current = state.activeStep
scrollview.current?.scrollTo({y: 0, animated: false}) scrollview.current?.scrollTo({y: 0, animated: false})
@@ -48,6 +48,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
const paddingTop = gtMobile ? a.py_5xl : a.py_lg const paddingTop = gtMobile ? a.py_5xl : a.py_lg
const dialogLabel = _(msg`Set up your account`) const dialogLabel = _(msg`Set up your account`)
const [headerHeight, setHeaderHeight] = useState(0)
const [footerHeight, setFooterHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0)
return ( return (
@@ -59,33 +60,12 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
accessibilityLabel={dialogLabel} accessibilityLabel={dialogLabel}
accessibilityHint={_(msg`Customizes your Bluesky experience`)} accessibilityHint={_(msg`Customizes your Bluesky experience`)}
style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
{__DEV__ && (
<Button
variant="ghost"
color="negative"
size="tiny"
onPress={() => onboardDispatch({type: 'skip'})}
// DEV ONLY
label="Clear onboarding state"
style={[
a.absolute,
a.z_10,
{
left: isWeb ? '50%' : Dimensions.get('window').width / 2 - 45,
top: insets.top + 2,
},
web({transform: [{translateX: '-50%'}]}),
]}>
<ButtonText>[DEV] Clear</ButtonText>
</Button>
)}
{!gtMobile && ( {!gtMobile && (
<View <View
pointerEvents="box-none"
style={[ style={[
web(a.fixed), web(a.fixed),
native(a.absolute), native(a.absolute),
a.top_0,
a.left_0, a.left_0,
a.right_0, a.right_0,
a.flex_row, a.flex_row,
@@ -93,85 +73,71 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
a.justify_center, a.justify_center,
a.z_20, a.z_20,
a.px_xl, a.px_xl,
{top: paddingTop.paddingTop + insets.top - 1}, {paddingTop: (web(paddingTop.paddingTop) ?? 0) + insets.top},
]}> native([t.atoms.bg, a.pb_xs, {minHeight: 48}]),
web(a.pointer_events_box_none),
]}
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
<View <View
pointerEvents="box-none"
style={[ style={[
a.w_full, a.w_full,
a.align_start, a.align_center,
a.flex_row, a.flex_row,
a.justify_between, a.justify_between,
{maxWidth: ONBOARDING_COL_WIDTH}, web({maxWidth: ONBOARDING_COL_WIDTH}),
web(a.pointer_events_box_none),
]}> ]}>
{state.canGoBack ? ( <HeaderSlot>
{state.canGoBack && (
<Button <Button
key={state.activeStep} // remove focus state on nav key={state.activeStep} // remove focus state on nav
color="secondary" color="secondary"
variant="ghost" variant="ghost"
shape="square" shape="round"
size="small" size="small"
label={_(msg`Go back to previous step`)} label={_(msg`Go back to previous step`)}
onPress={() => dispatch({type: 'prev'})} onPress={() => dispatch({type: 'prev'})}>
style={[a.bg_transparent]}>
<ButtonIcon icon={ArrowLeft} size="lg" /> <ButtonIcon icon={ArrowLeft} size="lg" />
</Button> </Button>
) : ( )}
<View /> </HeaderSlot>
{IS_INTERNAL && (
<Button
variant="ghost"
color="negative"
size="tiny"
onPress={() => onboardDispatch({type: 'skip'})}
// DEV ONLY
label="Clear onboarding state">
<ButtonText>[DEV] Clear</ButtonText>
</Button>
)} )}
<HeaderSlot>
<OnboardingHeaderSlot.Outlet /> <OnboardingHeaderSlot.Outlet />
</HeaderSlot>
</View> </View>
</View> </View>
)} )}
<ScrollView <ScrollView
ref={scrollview} ref={scrollview}
style={[a.h_full, a.w_full, {paddingTop: insets.top}]} style={[a.h_full, a.w_full, {paddingTop: isWeb ? insets.top : 0}]}
contentContainerStyle={{borderWidth: 0, minHeight: '100%'}} contentContainerStyle={{
borderWidth: 0,
minHeight: '100%',
paddingTop: headerHeight,
paddingBottom: footerHeight,
}}
showsVerticalScrollIndicator={!isAndroid} showsVerticalScrollIndicator={!isAndroid}
scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}} scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
// @ts-expect-error web only --prf // @ts-expect-error web only --prf
dataSet={{'stable-gutters': 1}}> dataSet={{'stable-gutters': 1}}>
<View <View
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}> style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
<View style={[a.flex_1, {maxWidth: ONBOARDING_COL_WIDTH}]}> <View style={[a.flex_1, web({maxWidth: ONBOARDING_COL_WIDTH})]}>
<View style={[a.w_full, a.align_center, paddingTop]}> <View style={[a.w_full, a.py_md]}>{children}</View>
<View
style={[
a.flex_row,
a.gap_sm,
a.w_full,
a.align_center,
{height: HEADER_SLOT_SIZE, maxWidth: '60%'},
]}>
{Array(state.totalSteps)
.fill(0)
.map((__, i) => (
<View
key={i}
style={[
a.flex_1,
a.pt_xs,
a.rounded_full,
t.atoms.bg_contrast_50,
{
backgroundColor:
i <= state.activeStepIndex
? t.palette.primary_500
: t.palette.contrast_100,
},
]}
/>
))}
</View>
</View>
<View style={[a.w_full, a.h_full, a.mb_5xl, a.pt_md]}>
{children}
</View>
<View style={{height: 100 + footerHeight}} />
</View> </View>
</View> </View>
</ScrollView> </ScrollView>
@@ -221,22 +187,46 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
) )
} }
export function TitleText({ function HeaderSlot({children}: {children?: React.ReactNode}) {
return (
<View style={[{minHeight: HEADER_SLOT_SIZE, minWidth: HEADER_SLOT_SIZE}]}>
{children}
</View>
)
}
export function OnboardingPosition() {
const {state} = useOnboardingInternalState()
const t = useTheme()
return (
<Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}>
<Trans>
Step {state.activeStepIndex + 1} of {state.totalSteps}
</Trans>
</Text>
)
}
export function OnboardingTitleText({
children, children,
style, style,
}: React.PropsWithChildren<TextStyleProp>) { }: React.PropsWithChildren<TextStyleProp>) {
return ( return (
<Text <Text style={[a.text_3xl, a.font_bold, a.leading_snug, style]}>
style={[a.pb_sm, a.text_4xl, a.font_semi_bold, a.leading_tight, style]}>
{children} {children}
</Text> </Text>
) )
} }
export function DescriptionText({ export function OnboardingDescriptionText({
children, children,
style, style,
}: React.PropsWithChildren<TextStyleProp>) { }: React.PropsWithChildren<TextStyleProp>) {
const t = useTheme() const t = useTheme()
return <P style={[t.atoms.text_contrast_medium, style]}>{children}</P> return (
<P style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium, style]}>
{children}
</P>
)
} }
@@ -4,18 +4,21 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQuery} from '@tanstack/react-query' import {useQuery} from '@tanstack/react-query'
import {atoms as a, useTheme} from '#/alf' import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {ContactsHeroImage} from '#/components/contacts/components/HeroImage' import {ContactsHeroImage} from '#/components/contacts/components/HeroImage'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {
import {OnboardingControls} from '../Layout' OnboardingControls,
OnboardingDescriptionText,
OnboardingPosition,
OnboardingTitleText,
} from '../Layout'
import {useOnboardingInternalState} from '../state' import {useOnboardingInternalState} from '../state'
export function StepFindContactsIntro() { export function StepFindContactsIntro() {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme()
const {dispatch} = useOnboardingInternalState() const {dispatch} = useOnboardingInternalState()
const {data: isAvailable, isSuccess} = useQuery({ const {data: isAvailable, isSuccess} = useQuery({
@@ -24,12 +27,13 @@ export function StepFindContactsIntro() {
}) })
return ( return (
<View style={[a.w_full, a.gap_lg]}> <View style={[a.w_full, a.gap_sm]}>
<OnboardingPosition />
<ContactsHeroImage /> <ContactsHeroImage />
<Text style={[a.text_3xl, a.leading_snug, a.font_bold]}> <OnboardingTitleText style={[a.mt_sm]}>
<Trans>Bluesky is more fun with friends</Trans> <Trans>Bluesky is more fun with friends</Trans>
</Text> </OnboardingTitleText>
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> <OnboardingDescriptionText>
<Trans> <Trans>
Find your friends on Bluesky by verifying your phone number and Find your friends on Bluesky by verifying your phone number and
matching with your contacts. We protect your information and you matching with your contacts. We protect your information and you
@@ -41,7 +45,7 @@ export function StepFindContactsIntro() {
TODO: Learn more TODO: Learn more
</InlineLinkText> </InlineLinkText>
</Trans> </Trans>
</Text> </OnboardingDescriptionText>
{!isAvailable && isSuccess && ( {!isAvailable && isSuccess && (
<Admonition type="error"> <Admonition type="error">
<Trans> <Trans>
@@ -57,7 +61,8 @@ export function StepFindContactsIntro() {
onPress={() => dispatch({type: 'next'})} onPress={() => dispatch({type: 'next'})}
label={_(msg`Import contacts`)} label={_(msg`Import contacts`)}
size="large" size="large"
color="primary"> color="primary"
disabled={!isAvailable}>
<ButtonText> <ButtonText>
<Trans>Import contacts</Trans> <Trans>Import contacts</Trans>
</ButtonText> </ButtonText>
+11 -14
View File
@@ -8,18 +8,16 @@ import {logEvent} from '#/lib/statsig/statsig'
import {capitalize} from '#/lib/strings/capitalize' import {capitalize} from '#/lib/strings/capitalize'
import {logger} from '#/logger' import {logger} from '#/logger'
import { import {
DescriptionText,
OnboardingControls, OnboardingControls,
TitleText, OnboardingDescriptionText,
OnboardingPosition,
OnboardingTitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {useOnboardingInternalState} from '#/screens/Onboarding/state'
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {IconCircle} from '#/components/IconCircle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
export function StepInterests() { export function StepInterests() {
@@ -53,17 +51,16 @@ export function StepInterests() {
}, [selectedInterests, setSaving, dispatch]) }, [selectedInterests, setSaving, dispatch])
return ( return (
<View style={[a.align_start]} testID="onboardingInterests"> <View style={[a.align_start, a.gap_sm]} testID="onboardingInterests">
<IconCircle icon={Hashtag} style={[a.mb_2xl]} /> <OnboardingPosition />
<OnboardingTitleText>
<TitleText>
<Trans>What are your interests?</Trans> <Trans>What are your interests?</Trans>
</TitleText> </OnboardingTitleText>
<DescriptionText> <OnboardingDescriptionText>
<Trans>We'll use this to help customize your experience.</Trans> <Trans>We'll use this to help customize your experience.</Trans>
</DescriptionText> </OnboardingDescriptionText>
<View style={[a.w_full, a.pt_2xl]}> <View style={[a.w_full, a.pt_lg]}>
<Toggle.Group <Toggle.Group
values={selectedInterests} values={selectedInterests}
onChange={setSelectedInterests} onChange={setSelectedInterests}
@@ -93,7 +90,7 @@ export function StepInterests() {
<ButtonText> <ButtonText>
<Trans>Continue</Trans> <Trans>Continue</Trans>
</ButtonText> </ButtonText>
<ButtonIcon icon={saving ? Loader : ChevronRight} /> {saving && <ButtonIcon icon={Loader} />}
</Button> </Button>
</OnboardingControls.Portal> </OnboardingControls.Portal>
</View> </View>
+16 -20
View File
@@ -19,9 +19,10 @@ import {isCancelledError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection' import {isNative, isWeb} from '#/platform/detection'
import { import {
DescriptionText,
OnboardingControls, OnboardingControls,
TitleText, OnboardingDescriptionText,
OnboardingPosition,
OnboardingTitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {useOnboardingInternalState} from '#/screens/Onboarding/state'
import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle' import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle'
@@ -32,13 +33,10 @@ import {
type PlaceholderCanvasRef, type PlaceholderCanvasRef,
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas' } from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {IconCircle} from '#/components/IconCircle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types' import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
@@ -237,19 +235,21 @@ export function StepProfile() {
return ( return (
<AvatarContext.Provider value={value}> <AvatarContext.Provider value={value}>
<View style={[a.align_start, t.atoms.bg, a.justify_between]}> <View style={[a.align_start]}>
<IconCircle icon={StreamingLive} style={[a.mb_2xl]} /> <View style={[a.gap_sm]}>
<TitleText> <OnboardingPosition />
<OnboardingTitleText>
<Trans>Give your profile a face</Trans> <Trans>Give your profile a face</Trans>
</TitleText> </OnboardingTitleText>
<DescriptionText> <OnboardingDescriptionText>
<Trans> <Trans>
Help people know you're not a bot by uploading a picture or creating Help people know you're not a bot by uploading a picture or
an avatar. creating an avatar.
</Trans> </Trans>
</DescriptionText> </OnboardingDescriptionText>
</View>
<View <View
style={[a.w_full, a.align_center, {paddingTop: gtMobile ? 80 : 40}]}> style={[a.w_full, a.align_center, {paddingTop: gtMobile ? 80 : 60}]}>
<AvatarCircle <AvatarCircle
openLibrary={openLibrary} openLibrary={openLibrary}
openCreator={creatorControl.open} openCreator={creatorControl.open}
@@ -279,7 +279,6 @@ export function StepProfile() {
<View style={[a.gap_md, gtMobile && a.flex_row_reverse]}> <View style={[a.gap_md, gtMobile && a.flex_row_reverse]}>
<Button <Button
testID="onboardingContinue" testID="onboardingContinue"
variant="solid"
color="primary" color="primary"
size="large" size="large"
label={_(msg`Continue to next step`)} label={_(msg`Continue to next step`)}
@@ -287,12 +286,10 @@ export function StepProfile() {
<ButtonText> <ButtonText>
<Trans>Continue</Trans> <Trans>Continue</Trans>
</ButtonText> </ButtonText>
<ButtonIcon icon={ChevronRight} position="right" />
</Button> </Button>
<Button <Button
testID="onboardingAvatarCreator" testID="onboardingAvatarCreator"
variant="ghost" color="primary_subtle"
color="primary"
size="large" size="large"
label={_(msg`Open avatar creator`)} label={_(msg`Open avatar creator`)}
onPress={onSecondaryPress}> onPress={onSecondaryPress}>
@@ -335,7 +332,6 @@ export function StepProfile() {
</View> </View>
<View style={[a.pt_4xl]}> <View style={[a.pt_4xl]}>
<Button <Button
variant="solid"
color="primary" color="primary"
size="large" size="large"
label={_(msg`Done`)} label={_(msg`Done`)}
@@ -15,7 +15,11 @@ import {updateProfileShadow} from '#/state/cache/profile-shadow'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {OnboardingControls} from '#/screens/Onboarding/Layout' import {
OnboardingControls,
OnboardingPosition,
OnboardingTitleText,
} from '#/screens/Onboarding/Layout'
import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {useOnboardingInternalState} from '#/screens/Onboarding/state'
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers' import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
@@ -27,7 +31,6 @@ import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import * as toast from '#/components/Toast' import * as toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {bulkWriteFollows} from '../util' import {bulkWriteFollows} from '../util'
@@ -146,17 +149,18 @@ export function StepSuggestedAccounts() {
) )
return ( return (
<View style={[a.align_start]} testID="onboardingInterests"> <View style={[a.align_start, a.gap_sm]} testID="onboardingInterests">
<Text style={[a.font_bold, a.text_3xl]}> <OnboardingPosition />
<OnboardingTitleText>
<Trans comment="Accounts suggested to the user for them to follow"> <Trans comment="Accounts suggested to the user for them to follow">
Suggested for you Suggested for you
</Trans> </Trans>
</Text> </OnboardingTitleText>
<View <View
style={[ style={[
a.overflow_hidden, a.overflow_hidden,
a.mt_lg, a.mt_sm,
isWeb ? a.max_w_full : {marginHorizontal: tokens.space.xl * -1}, isWeb ? a.max_w_full : {marginHorizontal: tokens.space.xl * -1},
a.flex_1, a.flex_1,
a.justify_start, a.justify_start,
@@ -4,14 +4,17 @@ import {useLingui} from '@lingui/react'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useOnboardingSuggestedStarterPacksQuery} from '#/state/queries/useOnboardingSuggestedStarterPacksQuery' import {useOnboardingSuggestedStarterPacksQuery} from '#/state/queries/useOnboardingSuggestedStarterPacksQuery'
import {OnboardingControls} from '#/screens/Onboarding/Layout' import {
OnboardingControls,
OnboardingPosition,
OnboardingTitleText,
} from '#/screens/Onboarding/Layout'
import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {useOnboardingInternalState} from '#/screens/Onboarding/state'
import {atoms as a, useBreakpoints} from '#/alf' import {atoms as a, useBreakpoints} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {StarterPackCard} from './StarterPackCard' import {StarterPackCard} from './StarterPackCard'
export function StepSuggestedStarterpacks() { export function StepSuggestedStarterpacks() {
@@ -33,26 +36,26 @@ export function StepSuggestedStarterpacks() {
}) })
return ( return (
<View style={[a.align_start]} testID="onboardingInterests"> <View style={[a.align_start, a.gap_sm]} testID="onboardingInterests">
<Text style={[a.font_bold, a.text_3xl]}> <OnboardingPosition />
<Trans comment="Accounts suggested to the user for them to follow"> <OnboardingTitleText>
Suggested for you <Trans comment="Starter packs suggested to the user for them to follow">
Find people to follow
</Trans> </Trans>
</Text> </OnboardingTitleText>
<View <View
style={[ style={[
a.overflow_hidden, a.overflow_hidden,
a.mt_lg,
a.flex_1, a.flex_1,
a.justify_start, a.justify_start,
a.w_full, a.w_full,
a.mt_sm,
]}> ]}>
{isLoading || !moderationOpts ? ( {isLoading || !moderationOpts ? (
<View <View
style={[ style={[
a.flex_1, a.flex_1,
a.mt_md,
a.align_center, a.align_center,
a.justify_center, a.justify_center,
{minHeight: 400}, {minHeight: 400},
@@ -68,7 +71,7 @@ export function StepSuggestedStarterpacks() {
</Admonition> </Admonition>
</View> </View>
) : ( ) : (
<View style={[a.flex_1, a.mt_md]}> <View style={[a.flex_1]}>
{suggestedStarterPacks?.starterPacks.map(starterPack => ( {suggestedStarterPacks?.starterPacks.map(starterPack => (
<View style={[a.pb_lg]} key={starterPack.uri}> <View style={[a.pb_lg]} key={starterPack.uri}>
<StarterPackCard view={starterPack} /> <StarterPackCard view={starterPack} />
+12 -2
View File
@@ -224,13 +224,23 @@ export function useOnboardingInternalState() {
return { return {
state: useMemo(() => { state: useMemo(() => {
const stepOrder = getStepOrder(state) const stepOrder = getStepOrder(state).filter(
x => x !== 'find-contacts' && x !== 'finished',
) as string[]
const canGoBack = state.activeStep !== stepOrder[0] const canGoBack = state.activeStep !== stepOrder[0]
return { return {
...state, ...state,
canGoBack, canGoBack,
/**
* Note: for *display* purposes only, do not lean on this
* for navigation purposes! we merge certain steps!
*/
activeStepIndex: stepOrder.indexOf(
state.activeStep === 'find-contacts'
? 'find-contacts-intro'
: state.activeStep,
),
totalSteps: stepOrder.length, totalSteps: stepOrder.length,
activeStepIndex: stepOrder.indexOf(state.activeStep),
} }
}, [state]), }, [state]),
dispatch, dispatch,