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 {VerifyNumber} from './screens/VerifyNumber'
import {ViewMatches} from './screens/ViewMatches'
import {type Action, type State} from './state'
import {type Action, FindContactsGoBackContext, type State} from './state'
export function FindContactsFlow({
state,
dispatch,
onBack,
onCancel,
context = 'Standalone',
}: {
state: State
dispatch: React.Dispatch<Action>
onBack?: () => void
onCancel: () => void
context: 'Onboarding' | 'Standalone'
}) {
return (
<>
<FindContactsGoBackContext value={onBack}>
{state.step === '1: phone input' && (
<PhoneInput
state={state}
@@ -37,8 +39,13 @@ export function FindContactsFlow({
<GetContacts state={state} dispatch={dispatch} onCancel={onCancel} />
)}
{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,
processPhoneNumber,
} from '../phone-number'
import {type Action, type State} from '../state'
import {type Action, type State, useOnPressBackButton} from '../state'
export function PhoneInput({
state,
@@ -103,10 +103,12 @@ export function PhoneInput({
const paddingBottom = Math.max(insets.bottom, tokens.space.xl)
const onPressBack = useOnPressBackButton()
return (
<View style={[a.h_full]}>
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.BackButton onPress={onPressBack} />
<Layout.Header.Content />
{showSkipButton ? (
<Button
@@ -204,12 +206,12 @@ export function PhoneInput({
<View style={[gutters, {paddingBottom}]}>
<Button
disabled={!phoneNumber || isPending}
label={_(msg`Next`)}
label={_(msg`Send code`)}
size="large"
color="primary"
onPress={onSubmitNumber}>
<ButtonText>
<Trans>Next</Trans>
<Trans>Send code</Trans>
</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
@@ -19,7 +19,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {OTPInput} from '../components/OTPInput'
import {type Action, type State} from '../state'
import {type Action, type State, useOnPressBackButton} from '../state'
export function VerifyNumber({
state,
dispatch,
@@ -121,10 +121,12 @@ export function VerifyNumber({
[state.phoneCountryCode],
)
const onPressBack = useOnPressBackButton()
return (
<View style={[a.h_full]}>
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.BackButton onPress={onPressBack} />
<Layout.Header.Content />
{showSkipButton ? (
<Button
@@ -1,5 +1,6 @@
import {useMemo, useRef, useState} from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as SMS from 'expo-sms'
import {type ModerationOpts} from '@atproto/api'
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 {UserAvatar} from '#/view/com/util/UserAvatar'
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 {SearchInput} from '#/components/forms/SearchInput'
import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -68,16 +69,22 @@ type Item =
export function ViewMatches({
state,
dispatch,
context,
onNext,
}: {
state: Extract<State, {step: '4: view matches'}>
dispatch: React.ActionDispatch<[Action]>
context: 'Onboarding' | 'Standalone'
onNext: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const t = useTheme()
const gutter = useGutters([0, 'wide'])
const moderationOpts = useModerationOpts()
const queryClient = useQueryClient()
const agent = useAgent()
const insets = useSafeAreaInsets()
const listRef = useRef<ListMethods>(null)
// TEMP!!!
@@ -322,13 +329,15 @@ export function ViewMatches({
return (
<View style={[a.h_full]}>
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
{context === 'Standalone' && (
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
)}
{!isTotallyEmpty && (
<View style={[gutter, a.mb_md]}>
<View style={[gutter, a.mb_md, context === 'Onboarding' && a.mt_sm]}>
<SearchInput
placeholder={_(msg`Search contacts`)}
value={search}
@@ -355,6 +364,29 @@ export function ViewMatches({
ListFooterComponent={!isEmpty ? <ListFooter /> : null}
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>
)
}
+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 CountryCode} from '#/lib/international-telephone-codes'
@@ -176,3 +177,17 @@ export function useFindContactsFlowState(
) {
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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {View} from 'react-native'
import * as bcp47Match from 'bcp-47-match'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {useLanguagePrefs} from '#/state/preferences'
@@ -19,15 +19,17 @@ import {
import {StepFinished} from '#/screens/Onboarding/StepFinished'
import {StepInterests} from '#/screens/Onboarding/StepInterests'
import {StepProfile} from '#/screens/Onboarding/StepProfile'
import {atoms as a, useTheme} from '#/alf'
import {Portal} from '#/components/Portal'
import {ScreenTransition} from '#/components/ScreenTransition'
import {ENV} from '#/env'
import {StepFindContacts} from './StepFindContacts'
import {StepSuggestedAccounts} from './StepSuggestedAccounts'
import {StepSuggestedStarterpacks} from './StepSuggestedStarterpacks'
export function Onboarding() {
const {_} = useLingui()
const gate = useGate()
const t = useTheme()
const {contentLanguages} = useLanguagePrefs()
const probablySpeaksEnglish = useMemo(() => {
@@ -52,61 +54,39 @@ export function Onboarding() {
createInitialOnboardingState,
)
const interestsDisplayNames = useMemo(() => {
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`),
}
}, [_])
useEnableKeyboardControllerScreen(true)
return (
<Portal>
<OnboardingControls.Provider>
<OnboardingHeaderSlot.Provider>
<Context.Provider
value={useMemo(
() => ({state, dispatch, interestsDisplayNames}),
[state, dispatch, interestsDisplayNames],
)}>
<Layout>
<View style={[a.absolute, a.inset_0, t.atoms.bg]}>
<OnboardingControls.Provider>
<OnboardingHeaderSlot.Provider>
<Context.Provider
value={useMemo(() => ({state, dispatch}), [state, dispatch])}>
<ScreenTransition
key={state.activeStep}
direction={state.stepTransitionDirection}>
{state.activeStep === 'profile' && <StepProfile />}
{state.activeStep === 'interests' && <StepInterests />}
{state.activeStep === 'suggested-accounts' && (
<StepSuggestedAccounts />
direction={state.stepTransitionDirection}
style={a.flex_1}>
{state.activeStep === 'find-contacts' ? (
<StepFindContacts />
) : (
<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>
</Layout>
</Context.Provider>
</OnboardingHeaderSlot.Provider>
</OnboardingControls.Provider>
</Context.Provider>
</OnboardingHeaderSlot.Provider>
</OnboardingControls.Provider>
</View>
</Portal>
)
}