integrate existing flow into onboarding

This commit is contained in:
Samuel Newman
2025-12-05 17:15:22 +02:00
parent e9a8cf5c26
commit d7a228e96f
7 changed files with 149 additions and 69 deletions
+11 -4
View File
@@ -2,21 +2,23 @@ import {GetContacts} from './screens/GetContacts'
import {PhoneInput} from './screens/PhoneInput' import {PhoneInput} from './screens/PhoneInput'
import {VerifyNumber} from './screens/VerifyNumber' import {VerifyNumber} from './screens/VerifyNumber'
import {ViewMatches} from './screens/ViewMatches' import {ViewMatches} from './screens/ViewMatches'
import {type Action, type State} from './state' import {type Action, FindContactsGoBackContext, type State} from './state'
export function FindContactsFlow({ export function FindContactsFlow({
state, state,
dispatch, dispatch,
onBack,
onCancel, onCancel,
context = 'Standalone', context = 'Standalone',
}: { }: {
state: State state: State
dispatch: React.Dispatch<Action> dispatch: React.Dispatch<Action>
onBack?: () => void
onCancel: () => void onCancel: () => void
context: 'Onboarding' | 'Standalone' context: 'Onboarding' | 'Standalone'
}) { }) {
return ( return (
<> <FindContactsGoBackContext value={onBack}>
{state.step === '1: phone input' && ( {state.step === '1: phone input' && (
<PhoneInput <PhoneInput
state={state} state={state}
@@ -37,8 +39,13 @@ export function FindContactsFlow({
<GetContacts state={state} dispatch={dispatch} onCancel={onCancel} /> <GetContacts state={state} dispatch={dispatch} onCancel={onCancel} />
)} )}
{state.step === '4: view matches' && ( {state.step === '4: view matches' && (
<ViewMatches state={state} dispatch={dispatch} /> <ViewMatches
state={state}
dispatch={dispatch}
context={context}
onNext={onCancel}
/>
)} )}
</> </FindContactsGoBackContext>
) )
} }
@@ -26,7 +26,7 @@ import {
getCountryCodeFromPastedNumber, getCountryCodeFromPastedNumber,
processPhoneNumber, processPhoneNumber,
} from '../phone-number' } from '../phone-number'
import {type Action, type State} from '../state' import {type Action, type State, useOnPressBackButton} from '../state'
export function PhoneInput({ export function PhoneInput({
state, state,
@@ -103,10 +103,12 @@ export function PhoneInput({
const paddingBottom = Math.max(insets.bottom, tokens.space.xl) const paddingBottom = Math.max(insets.bottom, tokens.space.xl)
const onPressBack = useOnPressBackButton()
return ( return (
<View style={[a.h_full]}> <View style={[a.h_full]}>
<Layout.Header.Outer noBottomBorder> <Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton /> <Layout.Header.BackButton onPress={onPressBack} />
<Layout.Header.Content /> <Layout.Header.Content />
{showSkipButton ? ( {showSkipButton ? (
<Button <Button
@@ -204,12 +206,12 @@ export function PhoneInput({
<View style={[gutters, {paddingBottom}]}> <View style={[gutters, {paddingBottom}]}>
<Button <Button
disabled={!phoneNumber || isPending} disabled={!phoneNumber || isPending}
label={_(msg`Next`)} label={_(msg`Send code`)}
size="large" size="large"
color="primary" color="primary"
onPress={onSubmitNumber}> onPress={onSubmitNumber}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> <Trans>Send code</Trans>
</ButtonText> </ButtonText>
{isPending && <ButtonIcon icon={Loader} />} {isPending && <ButtonIcon icon={Loader} />}
</Button> </Button>
@@ -19,7 +19,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {OTPInput} from '../components/OTPInput' import {OTPInput} from '../components/OTPInput'
import {type Action, type State} from '../state' import {type Action, type State, useOnPressBackButton} from '../state'
export function VerifyNumber({ export function VerifyNumber({
state, state,
dispatch, dispatch,
@@ -121,10 +121,12 @@ export function VerifyNumber({
[state.phoneCountryCode], [state.phoneCountryCode],
) )
const onPressBack = useOnPressBackButton()
return ( return (
<View style={[a.h_full]}> <View style={[a.h_full]}>
<Layout.Header.Outer noBottomBorder> <Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton /> <Layout.Header.BackButton onPress={onPressBack} />
<Layout.Header.Content /> <Layout.Header.Content />
{showSkipButton ? ( {showSkipButton ? (
<Button <Button
@@ -1,5 +1,6 @@
import {useMemo, useRef, useState} from 'react' import {useMemo, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as SMS from 'expo-sms' import * as SMS from 'expo-sms'
import {type ModerationOpts} from '@atproto/api' import {type ModerationOpts} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
@@ -19,7 +20,7 @@ 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 {bulkWriteFollows} from '#/screens/Onboarding/util' import {bulkWriteFollows} from '#/screens/Onboarding/util'
import {atoms as a, 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'
import {SearchInput} from '#/components/forms/SearchInput' import {SearchInput} from '#/components/forms/SearchInput'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -68,16 +69,22 @@ type Item =
export function ViewMatches({ export function ViewMatches({
state, state,
dispatch, dispatch,
context,
onNext,
}: { }: {
state: Extract<State, {step: '4: view matches'}> state: Extract<State, {step: '4: view matches'}>
dispatch: React.ActionDispatch<[Action]> dispatch: React.ActionDispatch<[Action]>
context: 'Onboarding' | 'Standalone'
onNext: () => void
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const t = useTheme()
const gutter = useGutters([0, 'wide']) const gutter = useGutters([0, 'wide'])
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const agent = useAgent()
const insets = useSafeAreaInsets()
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
// TEMP!!! // TEMP!!!
@@ -322,13 +329,15 @@ export function ViewMatches({
return ( return (
<View style={[a.h_full]}> <View style={[a.h_full]}>
<Layout.Header.Outer noBottomBorder> {context === 'Standalone' && (
<Layout.Header.BackButton /> <Layout.Header.Outer noBottomBorder>
<Layout.Header.Content /> <Layout.Header.BackButton />
<Layout.Header.Slot /> <Layout.Header.Content />
</Layout.Header.Outer> <Layout.Header.Slot />
</Layout.Header.Outer>
)}
{!isTotallyEmpty && ( {!isTotallyEmpty && (
<View style={[gutter, a.mb_md]}> <View style={[gutter, a.mb_md, context === 'Onboarding' && a.mt_sm]}>
<SearchInput <SearchInput
placeholder={_(msg`Search contacts`)} placeholder={_(msg`Search contacts`)}
value={search} value={search}
@@ -355,6 +364,29 @@ export function ViewMatches({
ListFooterComponent={!isEmpty ? <ListFooter /> : null} ListFooterComponent={!isEmpty ? <ListFooter /> : null}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
/> />
{context === 'Onboarding' && (
<View
style={[
t.atoms.bg,
t.atoms.border_contrast_low,
a.border_t,
a.align_center,
a.align_stretch,
gutter,
a.pt_md,
{paddingBottom: insets.bottom + tokens.space.md},
]}>
<Button
label={_(msg`Next`)}
onPress={onNext}
size="large"
color="primary">
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
</Button>
</View>
)}
</View> </View>
) )
} }
+16 -1
View File
@@ -1,4 +1,5 @@
import {useReducer} from 'react' import {createContext, useContext, useReducer} from 'react'
import {type GestureResponderEvent} from 'react-native'
import {type ExistingContact} from 'expo-contacts' import {type ExistingContact} from 'expo-contacts'
import {type CountryCode} from '#/lib/international-telephone-codes' import {type CountryCode} from '#/lib/international-telephone-codes'
@@ -176,3 +177,17 @@ export function useFindContactsFlowState(
) { ) {
return useReducer(reducer, initialState) return useReducer(reducer, initialState)
} }
export const FindContactsGoBackContext = createContext<
(() => void) | undefined
>(undefined)
export function useOnPressBackButton() {
const goBack = useContext(FindContactsGoBackContext)
if (!goBack) {
return undefined
}
return (evt: GestureResponderEvent) => {
evt.preventDefault()
goBack()
}
}
@@ -0,0 +1,42 @@
import {useCallback} from 'react'
import {LayoutAnimationConfig} from 'react-native-reanimated'
import {SafeAreaView} from 'react-native-safe-area-context'
import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
import {useFindContactsFlowState} from '#/components/contacts/state'
import {ScreenTransition} from '#/components/ScreenTransition'
import {useOnboardingInternalState} from '../state'
export function StepFindContacts() {
const [fcfState, fcfDispatch] = useFindContactsFlowState()
const {dispatch} = useOnboardingInternalState()
const onSkip = useCallback(() => {
dispatch({type: 'next'})
}, [dispatch])
const canGoBack = fcfState.step === '2: verify number'
const onBack = useCallback(() => {
if (canGoBack) {
fcfDispatch({type: 'BACK'})
} else {
dispatch({type: 'prev'})
}
}, [dispatch, fcfDispatch, canGoBack])
return (
<SafeAreaView edges={['left', 'top', 'right']}>
<LayoutAnimationConfig skipEntering skipExiting>
<ScreenTransition key={fcfState.step} direction="Forward">
<FindContactsFlow
context="Onboarding"
state={fcfState}
dispatch={fcfDispatch}
onCancel={onSkip}
onBack={onBack}
/>
</ScreenTransition>
</LayoutAnimationConfig>
</SafeAreaView>
)
}
+31 -51
View File
@@ -1,8 +1,8 @@
import {useMemo, useReducer} from 'react' import {useMemo, useReducer} from 'react'
import {msg} from '@lingui/macro' import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import * as bcp47Match from 'bcp-47-match' import * as bcp47Match from 'bcp-47-match'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
@@ -19,15 +19,17 @@ import {
import {StepFinished} from '#/screens/Onboarding/StepFinished' import {StepFinished} from '#/screens/Onboarding/StepFinished'
import {StepInterests} from '#/screens/Onboarding/StepInterests' import {StepInterests} from '#/screens/Onboarding/StepInterests'
import {StepProfile} from '#/screens/Onboarding/StepProfile' import {StepProfile} from '#/screens/Onboarding/StepProfile'
import {atoms as a, useTheme} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {ScreenTransition} from '#/components/ScreenTransition' import {ScreenTransition} from '#/components/ScreenTransition'
import {ENV} from '#/env' import {ENV} from '#/env'
import {StepFindContacts} from './StepFindContacts'
import {StepSuggestedAccounts} from './StepSuggestedAccounts' import {StepSuggestedAccounts} from './StepSuggestedAccounts'
import {StepSuggestedStarterpacks} from './StepSuggestedStarterpacks' import {StepSuggestedStarterpacks} from './StepSuggestedStarterpacks'
export function Onboarding() { export function Onboarding() {
const {_} = useLingui()
const gate = useGate() const gate = useGate()
const t = useTheme()
const {contentLanguages} = useLanguagePrefs() const {contentLanguages} = useLanguagePrefs()
const probablySpeaksEnglish = useMemo(() => { const probablySpeaksEnglish = useMemo(() => {
@@ -52,61 +54,39 @@ export function Onboarding() {
createInitialOnboardingState, createInitialOnboardingState,
) )
const interestsDisplayNames = useMemo(() => { useEnableKeyboardControllerScreen(true)
return {
news: _(msg`News`),
journalism: _(msg`Journalism`),
nature: _(msg`Nature`),
art: _(msg`Art`),
comics: _(msg`Comics`),
writers: _(msg`Writers`),
culture: _(msg`Culture`),
sports: _(msg`Sports`),
pets: _(msg`Pets`),
animals: _(msg`Animals`),
books: _(msg`Books`),
education: _(msg`Education`),
climate: _(msg`Climate`),
science: _(msg`Science`),
politics: _(msg`Politics`),
fitness: _(msg`Fitness`),
tech: _(msg`Tech`),
dev: _(msg`Software Dev`),
comedy: _(msg`Comedy`),
gaming: _(msg`Video Games`),
food: _(msg`Food`),
cooking: _(msg`Cooking`),
}
}, [_])
return ( return (
<Portal> <Portal>
<OnboardingControls.Provider> <View style={[a.absolute, a.inset_0, t.atoms.bg]}>
<OnboardingHeaderSlot.Provider> <OnboardingControls.Provider>
<Context.Provider <OnboardingHeaderSlot.Provider>
value={useMemo( <Context.Provider
() => ({state, dispatch, interestsDisplayNames}), value={useMemo(() => ({state, dispatch}), [state, dispatch])}>
[state, dispatch, interestsDisplayNames],
)}>
<Layout>
<ScreenTransition <ScreenTransition
key={state.activeStep} key={state.activeStep}
direction={state.stepTransitionDirection}> direction={state.stepTransitionDirection}
{state.activeStep === 'profile' && <StepProfile />} style={a.flex_1}>
{state.activeStep === 'interests' && <StepInterests />} {state.activeStep === 'find-contacts' ? (
{state.activeStep === 'suggested-accounts' && ( <StepFindContacts />
<StepSuggestedAccounts /> ) : (
<Layout>
{state.activeStep === 'profile' && <StepProfile />}
{state.activeStep === 'interests' && <StepInterests />}
{state.activeStep === 'suggested-accounts' && (
<StepSuggestedAccounts />
)}
{state.activeStep === 'suggested-starterpacks' && (
<StepSuggestedStarterpacks />
)}
{state.activeStep === 'finished' && <StepFinished />}
</Layout>
)} )}
{state.activeStep === 'suggested-starterpacks' && (
<StepSuggestedStarterpacks />
)}
{state.activeStep === 'find-contacts' && null}
{state.activeStep === 'finished' && <StepFinished />}
</ScreenTransition> </ScreenTransition>
</Layout> </Context.Provider>
</Context.Provider> </OnboardingHeaderSlot.Provider>
</OnboardingHeaderSlot.Provider> </OnboardingControls.Provider>
</OnboardingControls.Provider> </View>
</Portal> </Portal>
) )
} }