From d4dedf463a76165375e1651ed4ce9262219ac10f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 10 Dec 2025 21:55:23 +0200 Subject: [PATCH] [Contacts] Onboarding step (#9489) * add a bunch of number parsing logic with libphonenumber * restructure onboarding to better support dynamic screens * integrate existing flow into onboarding * add intro step * lift state up to allow going back freely * gate onboarding by geo if unsupported country * add done button to standalone flow --- src/components/contacts/FindContactsFlow.tsx | 17 +- .../contacts/screens/PhoneInput.tsx | 10 +- .../contacts/screens/VerifyNumber.tsx | 6 +- .../contacts/screens/ViewMatches.tsx | 47 ++++- src/components/contacts/state.ts | 17 +- src/lib/statsig/gates.ts | 1 + src/screens/Onboarding/Layout.tsx | 10 +- .../Onboarding/StepFindContacts/index.tsx | 55 ++++++ .../StepFindContactsIntro/index.tsx | 78 +++++++++ src/screens/Onboarding/StepFinished/index.tsx | 114 +------------ .../Onboarding/StepInterests/index.tsx | 4 +- src/screens/Onboarding/StepProfile/index.tsx | 4 +- .../StepSuggestedAccounts/index.tsx | 13 +- .../StepSuggestedStarterpacks/index.tsx | 5 +- src/screens/Onboarding/index.tsx | 121 ++++++------- src/screens/Onboarding/state.ts | 161 ++++++++++++------ 16 files changed, 402 insertions(+), 261 deletions(-) create mode 100644 src/screens/Onboarding/StepFindContacts/index.tsx create mode 100644 src/screens/Onboarding/StepFindContactsIntro/index.tsx diff --git a/src/components/contacts/FindContactsFlow.tsx b/src/components/contacts/FindContactsFlow.tsx index ecacf3f3f1..6cd424b6cb 100644 --- a/src/components/contacts/FindContactsFlow.tsx +++ b/src/components/contacts/FindContactsFlow.tsx @@ -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 + dispatch: React.ActionDispatch<[Action]> + onBack?: () => void onCancel: () => void context: 'Onboarding' | 'Standalone' }) { return ( - <> + {state.step === '1: phone input' && ( )} {state.step === '4: view matches' && ( - + )} - + ) } diff --git a/src/components/contacts/screens/PhoneInput.tsx b/src/components/contacts/screens/PhoneInput.tsx index b9124ac58e..55fd94adaa 100644 --- a/src/components/contacts/screens/PhoneInput.tsx +++ b/src/components/contacts/screens/PhoneInput.tsx @@ -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 ( - + {showSkipButton ? ( diff --git a/src/components/contacts/screens/VerifyNumber.tsx b/src/components/contacts/screens/VerifyNumber.tsx index e765585f65..ed746ece97 100644 --- a/src/components/contacts/screens/VerifyNumber.tsx +++ b/src/components/contacts/screens/VerifyNumber.tsx @@ -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 ( - + {showSkipButton ? ( + ) } diff --git a/src/components/contacts/state.ts b/src/components/contacts/state.ts index 8e134db40a..3949f901e3 100644 --- a/src/components/contacts/state.ts +++ b/src/components/contacts/state.ts @@ -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() + } +} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index ea263e22e5..8c757a0160 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -3,6 +3,7 @@ export type Gate = | 'alt_share_icon' | 'debug_show_feedcontext' | 'debug_subscriptions' + | 'disable_onboarding_find_contacts' | 'explore_show_suggested_feeds' | 'feed_reply_button_open_thread' | 'old_postonboarding' diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx index 84448d97d4..95607214f6 100644 --- a/src/screens/Onboarding/Layout.tsx +++ b/src/screens/Onboarding/Layout.tsx @@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react' import {isAndroid, isWeb} from '#/platform/detection' import {useOnboardingDispatch} from '#/state/shell' -import {Context} from '#/screens/Onboarding/state' +import {useOnboardingInternalState} from '#/screens/Onboarding/state' import { atoms as a, native, @@ -34,7 +34,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) { const insets = useSafeAreaInsets() const {gtMobile} = useBreakpoints() const onboardDispatch = useOnboardingDispatch() - const {state, dispatch} = React.useContext(Context) + const {state, dispatch} = useOnboardingInternalState() const scrollview = React.useRef(null) const prevActiveStep = React.useRef(state.activeStep) @@ -104,7 +104,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) { a.justify_between, {maxWidth: ONBOARDING_COL_WIDTH}, ]}> - {state.hasPrev ? ( + {state.canGoBack ? ( + + + + + ) +} diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index c1cb3f02f0..abf21d51d5 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useContext, useState} from 'react' +import {useCallback, useState} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, @@ -35,28 +35,23 @@ import { useSetActiveStarterPack, } from '#/state/shell/starter-pack' import { - DescriptionText, OnboardingControls, OnboardingHeaderSlot, - TitleText, } from '#/screens/Onboarding/Layout' -import {Context, type OnboardingState} from '#/screens/Onboarding/state' +import { + type OnboardingState, + useOnboardingInternalState, +} from '#/screens/Onboarding/state' import {bulkWriteFollows} from '#/screens/Onboarding/util' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, useBreakpoints} 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' -import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending' import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' import {ValuePropositionPager} from './ValuePropositionPager' export function StepFinished() { - const {state, dispatch} = useContext(Context) + const {state, dispatch} = useOnboardingInternalState() const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = useState(false) const queryClient = useQueryClient() @@ -247,18 +242,12 @@ export function StepFinished() { gate, ]) - return state.experiments?.onboarding_value_prop ? ( + return ( - ) : ( - ) } @@ -359,90 +348,3 @@ function ValueProposition({ ) } - -function LegacyFinalStep({ - finishOnboarding, - saving, - state, -}: { - finishOnboarding: () => void - saving: boolean - state: OnboardingState -}) { - const t = useTheme() - const {_} = useLingui() - - return ( - - - - - You're ready to go! - - - We hope you have a wonderful time. Remember, Bluesky is: - - - - - - - - Public - - - - Your posts, likes, and blocks are public. Mutes are private. - - - - - - - - - Open - - - Never lose access to your followers or data. - - - - - - - - Flexible - - - Choose the algorithms that power your custom feeds. - - - - - - - - - - ) -} diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx index 3e51409757..ee28601e38 100644 --- a/src/screens/Onboarding/StepInterests/index.tsx +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -12,7 +12,7 @@ import { OnboardingControls, TitleText, } from '#/screens/Onboarding/Layout' -import {Context} from '#/screens/Onboarding/state' +import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -26,7 +26,7 @@ export function StepInterests() { const {_} = useLingui() const interestsDisplayNames = useInterestsDisplayNames() - const {state, dispatch} = React.useContext(Context) + const {state, dispatch} = useOnboardingInternalState() const [saving, setSaving] = React.useState(false) const [selectedInterests, setSelectedInterests] = React.useState( state.interestsStepResults.selectedInterests.map(i => i), diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 453184639a..18ffced8a7 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -23,7 +23,7 @@ import { OnboardingControls, TitleText, } from '#/screens/Onboarding/Layout' -import {Context} from '#/screens/Onboarding/state' +import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle' import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle' import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems' @@ -78,7 +78,7 @@ export function StepProfile() { const creatorControl = Dialog.useDialogControl() const [error, setError] = React.useState('') - const {state, dispatch} = React.useContext(Context) + const {state, dispatch} = useOnboardingInternalState() const [avatar, setAvatar] = React.useState({ image: state.profileStepResults?.image, placeholder: state.profileStepResults.creatorState?.emoji || emojiItems.at, diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 200ccb71e6..1219d3e98b 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -1,11 +1,4 @@ -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react' +import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {type ModerationOpts} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -23,7 +16,7 @@ import {useLanguagePrefs} from '#/state/preferences' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent, useSession} from '#/state/session' import {OnboardingControls} from '#/screens/Onboarding/Layout' -import {Context} from '#/screens/Onboarding/state' +import {useOnboardingInternalState} 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' @@ -47,7 +40,7 @@ export function StepSuggestedAccounts() { const {currentAccount} = useSession() const queryClient = useQueryClient() - const {state, dispatch} = useContext(Context) + const {state, dispatch} = useOnboardingInternalState() const [selectedInterest, setSelectedInterest] = useState(null) // keeping track of who was followed via the follow all button diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx b/src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx index 5ba00ef7d1..fcd37a329c 100644 --- a/src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx @@ -1,4 +1,3 @@ -import {useContext} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -6,7 +5,7 @@ import {useLingui} from '@lingui/react' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useOnboardingSuggestedStarterPacksQuery} from '#/state/queries/useOnboardingSuggestedStarterPacksQuery' import {OnboardingControls} from '#/screens/Onboarding/Layout' -import {Context} from '#/screens/Onboarding/state' +import {useOnboardingInternalState} from '#/screens/Onboarding/state' import {atoms as a, useBreakpoints} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -20,7 +19,7 @@ export function StepSuggestedStarterpacks() { const {gtMobile} = useBreakpoints() const moderationOpts = useModerationOpts() - const {state, dispatch} = useContext(Context) + const {state, dispatch} = useOnboardingInternalState() const { data: suggestedStarterPacks, diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx index adf6a3f618..5c08f40cd0 100644 --- a/src/screens/Onboarding/index.tsx +++ b/src/screens/Onboarding/index.tsx @@ -1,28 +1,38 @@ 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' import { Layout, OnboardingControls, OnboardingHeaderSlot, } from '#/screens/Onboarding/Layout' -import {Context, initialState, reducer} from '#/screens/Onboarding/state' +import { + Context, + createInitialOnboardingState, + reducer, +} from '#/screens/Onboarding/state' 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 {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-whitelist' +import {useFindContactsFlowState} from '#/components/contacts/state' import {Portal} from '#/components/Portal' import {ScreenTransition} from '#/components/ScreenTransition' import {ENV} from '#/env' +import {StepFindContacts} from './StepFindContacts' +import {StepFindContactsIntro} from './StepFindContactsIntro' 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(() => { @@ -36,70 +46,61 @@ export function Onboarding() { probablySpeaksEnglish && gate('onboarding_suggested_starterpacks') - const [state, dispatch] = useReducer(reducer, { - ...initialState, - totalSteps: 4 + (showSuggestedStarterpacks ? 1 : 0), - experiments: { - onboarding_suggested_accounts: true, - onboarding_value_prop: true, - onboarding_suggested_starterpacks: showSuggestedStarterpacks, - }, - }) + const findContactsEnabled = + useIsFindContactsFeatureEnabledBasedOnGeolocation() + const showFindContacts = + isNative && findContactsEnabled && !gate('disable_onboarding_find_contacts') - 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`), - } - }, [_]) + const [state, dispatch] = useReducer( + reducer, + { + starterPacksStepEnabled: showSuggestedStarterpacks, + findContactsStepEnabled: showFindContacts, + }, + createInitialOnboardingState, + ) + const [contactsFlowState, contactsFlowDispatch] = useFindContactsFlowState() + + useEnableKeyboardControllerScreen(true) return ( - - - ({state, dispatch, interestsDisplayNames}), - [state, dispatch, interestsDisplayNames], - )}> - + + + + ({state, dispatch}), [state, dispatch])}> - {state.activeStep === 'profile' && } - {state.activeStep === 'interests' && } - {state.activeStep === 'suggested-accounts' && ( - + direction={state.stepTransitionDirection} + style={a.flex_1}> + {/* FindContactsFlow cannot be nested in Layout */} + {state.activeStep === 'find-contacts' ? ( + + ) : ( + + {state.activeStep === 'profile' && } + {state.activeStep === 'interests' && } + {state.activeStep === 'suggested-accounts' && ( + + )} + {state.activeStep === 'suggested-starterpacks' && ( + + )} + {state.activeStep === 'find-contacts-intro' && ( + + )} + {state.activeStep === 'finished' && } + )} - {state.activeStep === 'suggested-starterpacks' && ( - - )} - {state.activeStep === 'finished' && } - - - - + + + + ) } diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts index e9877a3d6b..8d66a6d270 100644 --- a/src/screens/Onboarding/state.ts +++ b/src/screens/Onboarding/state.ts @@ -1,4 +1,4 @@ -import React from 'react' +import {createContext, useContext, useMemo} from 'react' import {logger} from '#/logger' import { @@ -6,16 +6,18 @@ import { type Emoji, } from '#/screens/Onboarding/StepProfile/types' +type OnboardingScreen = + | 'profile' + | 'interests' + | 'suggested-accounts' + | 'suggested-starterpacks' + | 'find-contacts-intro' + | 'find-contacts' + | 'finished' + export type OnboardingState = { - hasPrev: boolean - totalSteps: number - activeStep: - | 'profile' - | 'interests' - | 'suggested-accounts' - | 'suggested-starterpacks' - | 'finished' - activeStepIndex: number + screens: Record + activeStep: OnboardingScreen stepTransitionDirection: 'Forward' | 'Backward' interestsStepResults: { @@ -37,12 +39,6 @@ export type OnboardingState = { backgroundColor: AvatarColor } } - - experiments?: { - onboarding_suggested_accounts?: boolean - onboarding_value_prop?: boolean - onboarding_suggested_starterpacks?: boolean - } } export type OnboardingAction = @@ -52,6 +48,9 @@ export type OnboardingAction = | { type: 'prev' } + | { + type: 'skip-contacts' + } | { type: 'finish' } @@ -73,31 +72,45 @@ export type OnboardingAction = | undefined } -export const initialState: OnboardingState = { - hasPrev: false, - totalSteps: 3, - activeStep: 'profile', - activeStepIndex: 1, - stepTransitionDirection: 'Forward', +export function createInitialOnboardingState( + { + starterPacksStepEnabled, + findContactsStepEnabled, + }: { + starterPacksStepEnabled: boolean + findContactsStepEnabled: boolean + } = {starterPacksStepEnabled: true, findContactsStepEnabled: false}, +): OnboardingState { + const screens: OnboardingState['screens'] = { + profile: true, + interests: true, + 'suggested-accounts': true, + 'suggested-starterpacks': starterPacksStepEnabled, + 'find-contacts-intro': findContactsStepEnabled, + 'find-contacts': findContactsStepEnabled, + finished: true, + } - interestsStepResults: { - selectedInterests: [], - }, - profileStepResults: { - isCreatedAvatar: false, - image: undefined, - imageUri: '', - imageMime: '', - }, + return { + screens, + activeStep: 'profile', + stepTransitionDirection: 'Forward', + interestsStepResults: { + selectedInterests: [], + }, + profileStepResults: { + isCreatedAvatar: false, + image: undefined, + imageUri: '', + imageMime: '', + }, + } } -export const Context = React.createContext<{ +export const Context = createContext<{ state: OnboardingState dispatch: React.Dispatch -}>({ - state: {...initialState}, - dispatch: () => {}, -}) +} | null>(null) Context.displayName = 'OnboardingContext' export function reducer( @@ -106,42 +119,39 @@ export function reducer( ): OnboardingState { let next = {...s} - const stepOrder: OnboardingState['activeStep'][] = [ - 'profile', - 'interests', - ...(s.experiments?.onboarding_suggested_accounts - ? (['suggested-accounts'] as const) - : []), - ...(s.experiments?.onboarding_suggested_starterpacks - ? (['suggested-starterpacks'] as const) - : []), - 'finished', - ] + const stepOrder = getStepOrder(s) switch (a.type) { case 'next': { - // 1-indexed for some reason - const nextIndex = s.activeStepIndex + const nextIndex = stepOrder.indexOf(next.activeStep) + 1 const nextStep = stepOrder[nextIndex] if (nextStep) { next.activeStep = nextStep - next.activeStepIndex = nextIndex + 1 } next.stepTransitionDirection = 'Forward' break } case 'prev': { - const prevIndex = s.activeStepIndex - 2 + const prevIndex = stepOrder.indexOf(next.activeStep) - 1 const prevStep = stepOrder[prevIndex] if (prevStep) { next.activeStep = prevStep - next.activeStepIndex = prevIndex + 1 } next.stepTransitionDirection = 'Backward' break } + case 'skip-contacts': { + const nextIndex = stepOrder.indexOf('find-contacts') + 1 + const nextStep = stepOrder[nextIndex] ?? 'finished' + next.activeStep = nextStep + next.stepTransitionDirection = 'Forward' + break + } case 'finish': { - next = initialState + next = createInitialOnboardingState({ + starterPacksStepEnabled: s.screens['suggested-starterpacks'], + findContactsStepEnabled: s.screens['find-contacts'], + }) break } case 'setInterestsStepResults': { @@ -170,7 +180,6 @@ export function reducer( logger.debug(`onboarding`, { hasPrev: state.hasPrev, activeStep: state.activeStep, - activeStepIndex: state.activeStepIndex, interestsStepResults: { selectedInterests: state.interestsStepResults.selectedInterests, }, @@ -183,3 +192,47 @@ export function reducer( return state } + +function getStepOrder(s: OnboardingState): OnboardingScreen[] { + return [ + s.screens.profile && ('profile' as const), + s.screens.interests && ('interests' as const), + s.screens['suggested-accounts'] && ('suggested-accounts' as const), + s.screens['suggested-starterpacks'] && ('suggested-starterpacks' as const), + s.screens['find-contacts-intro'] && ('find-contacts-intro' as const), + s.screens['find-contacts'] && ('find-contacts' as const), + s.screens.finished && ('finished' as const), + ].filter(x => !!x) +} + +/** + * Note: not to be confused with `useOnboardingState`, which just determines if onboarding is active. + * This hook is for internal state of the onboarding flow (i.e. active step etc). + * + * This adds additional derived state to the onboarding context reducer. + */ +export function useOnboardingInternalState() { + const ctx = useContext(Context) + + if (!ctx) { + throw new Error( + 'useOnboardingInternalState must be used within OnboardingContext', + ) + } + + const {state, dispatch} = ctx + + return { + state: useMemo(() => { + const stepOrder = getStepOrder(state) + const canGoBack = state.activeStep !== stepOrder[0] + return { + ...state, + canGoBack, + totalSteps: stepOrder.length, + activeStepIndex: stepOrder.indexOf(state.activeStep), + } + }, [state]), + dispatch, + } +}