From 43e44b9bc25b95918d803702ecf5ac3fb366c4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Fri, 13 Mar 2026 15:12:36 -0600 Subject: [PATCH 1/4] Add rhpHomePage variant (D) to onboarding A/B/C/D test Add frontend handling for the new rhpHomePage variant that navigates users to the Home page after onboarding, without opening the side panel or test drive modal. Concierge Anywhere remains accessible in the #admins room naturally. Co-Authored-By: Claude Opus 4.6 --- src/CONST/index.ts | 1 + src/components/SidePanel/RHPVariantTest/index.ts | 9 ++++++++- src/types/onyx/OnboardingRHPVariant.ts | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index f2d66c6656c65..ab9a72ed9772f 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6373,6 +6373,7 @@ const CONST = { ONBOARDING_RHP_VARIANT: { RHP_CONCIERGE_DM: 'rhpConciergeDm', RHP_ADMINS_ROOM: 'rhpAdminsRoom', + RHP_HOME_PAGE: 'rhpHomePage', CONTROL: 'control', }, ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?', diff --git a/src/components/SidePanel/RHPVariantTest/index.ts b/src/components/SidePanel/RHPVariantTest/index.ts index 43cfe8e7d11d2..3871a124b49c2 100644 --- a/src/components/SidePanel/RHPVariantTest/index.ts +++ b/src/components/SidePanel/RHPVariantTest/index.ts @@ -34,8 +34,9 @@ const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => { const isMicroCompany = onboardingCompanySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO; const isRHPConciergeDM = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_CONCIERGE_DM; const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM; + const isRHPHomePage = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE; - return isMicroCompany && (isRHPConciergeDM || isRHPAdminsRoom); + return isMicroCompany && (isRHPConciergeDM || isRHPAdminsRoom || isRHPHomePage); }; /** @@ -43,8 +44,14 @@ const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => { * - Control: navigate to the last accessed report on small screens, do not open side panel * - RHP Concierge DM: navigate to the workspace overview and open the side panel with the Concierge DM * - RHP Admins Room: navigate to the workspace overview and open the side panel with the Admins Room + * - RHP Home Page: navigate to the Home page without opening the side panel or test drive modal */ const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID) => { + if (onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE) { + Navigation.navigate(ROUTES.HOME); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(onboardingPolicyID)); SidePanelActions.openSidePanel(true); }; diff --git a/src/types/onyx/OnboardingRHPVariant.ts b/src/types/onyx/OnboardingRHPVariant.ts index 271e1977e7370..9ac25b94c31cc 100644 --- a/src/types/onyx/OnboardingRHPVariant.ts +++ b/src/types/onyx/OnboardingRHPVariant.ts @@ -1,9 +1,10 @@ /** - * The variant of the onboarding RHP for A/B/C testing + * The variant of the onboarding RHP for A/B/C/D testing * @description 'control' - The variant with the Concierge DM * @description 'rhpConciergeDm' - Admin of workspace with Concierge DM * @description 'rhpAdminsRoom' - Admin of workspace with the admins room + * @description 'rhpHomePage' - Navigate to Home page with Concierge Anywhere accessible in #admins room */ -type OnboardingRHPVariant = 'rhpConciergeDm' | 'rhpAdminsRoom' | 'control'; +type OnboardingRHPVariant = 'rhpConciergeDm' | 'rhpAdminsRoom' | 'rhpHomePage' | 'control'; export default OnboardingRHPVariant; From 1e7a8503ed47dca112e801fb4e13f0c2e13a38c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Fri, 13 Mar 2026 15:25:47 -0600 Subject: [PATCH 2/4] Fix race condition: pass RHP variant from API response to navigation The module-level onboardingRHPVariant variable in RHPVariantTest/index.ts is populated by an Onyx.connectWithoutView callback that may not have fired by the time navigateAfterOnboarding calls shouldOpenRHPVariant. This causes intermittent failures where users assigned to rhpConciergeDm or rhpAdminsRoom fall through to default navigation instead of the workspace editor. Fix: accept an optional variantOverride parameter in shouldOpenRHPVariant and handleRHPVariantNavigation. The caller in BaseOnboardingInterestedFeatures extracts the variant directly from the CompleteGuidedSetup API response onyxData and passes it through, bypassing the async Onyx callback entirely. Co-Authored-By: Claude Opus 4.6 --- .../SidePanel/RHPVariantTest/index.native.ts | 6 ++++-- .../SidePanel/RHPVariantTest/index.ts | 20 +++++++++++++------ .../SidePanel/RHPVariantTest/types.ts | 6 ++++-- src/libs/navigateAfterOnboarding.ts | 8 ++++++-- .../BaseOnboardingInterestedFeatures.tsx | 9 ++++++++- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/components/SidePanel/RHPVariantTest/index.native.ts b/src/components/SidePanel/RHPVariantTest/index.native.ts index 466080a1b07d8..53127b64b42c4 100644 --- a/src/components/SidePanel/RHPVariantTest/index.native.ts +++ b/src/components/SidePanel/RHPVariantTest/index.native.ts @@ -3,11 +3,13 @@ import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types'; /** * Side Panel is not supported on native platforms, so we always return false. */ -const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => false; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const shouldOpenRHPVariant: ShouldOpenRHPVariant = (_variantOverride) => false; /** * No-op on native platforms since Side Panel is not supported. */ -const handleRHPVariantNavigation: HandleRHPVariantNavigation = () => {}; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const handleRHPVariantNavigation: HandleRHPVariantNavigation = (_onboardingPolicyID, _variantOverride) => {}; export {shouldOpenRHPVariant, handleRHPVariantNavigation}; diff --git a/src/components/SidePanel/RHPVariantTest/index.ts b/src/components/SidePanel/RHPVariantTest/index.ts index 3871a124b49c2..6cd1ab028ec07 100644 --- a/src/components/SidePanel/RHPVariantTest/index.ts +++ b/src/components/SidePanel/RHPVariantTest/index.ts @@ -29,12 +29,17 @@ Onyx.connectWithoutView({ /** * Determines if the user should be navigated to the RHP variant side panel after onboarding. * The RHP variant is only shown to micro companies that are part of the RHP experiment. + * + * Accepts an optional variantOverride to bypass the module-level Onyx variable, avoiding a race + * condition where the Onyx callback hasn't fired yet when this is called immediately after the + * CompleteGuidedSetup API response. */ -const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => { +const shouldOpenRHPVariant: ShouldOpenRHPVariant = (variantOverride) => { + const variant = variantOverride ?? onboardingRHPVariant; const isMicroCompany = onboardingCompanySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO; - const isRHPConciergeDM = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_CONCIERGE_DM; - const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM; - const isRHPHomePage = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE; + const isRHPConciergeDM = variant === CONST.ONBOARDING_RHP_VARIANT.RHP_CONCIERGE_DM; + const isRHPAdminsRoom = variant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM; + const isRHPHomePage = variant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE; return isMicroCompany && (isRHPConciergeDM || isRHPAdminsRoom || isRHPHomePage); }; @@ -45,9 +50,12 @@ const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => { * - RHP Concierge DM: navigate to the workspace overview and open the side panel with the Concierge DM * - RHP Admins Room: navigate to the workspace overview and open the side panel with the Admins Room * - RHP Home Page: navigate to the Home page without opening the side panel or test drive modal + * + * Accepts an optional variantOverride for the same race-condition reason as shouldOpenRHPVariant. */ -const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID) => { - if (onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE) { +const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID, variantOverride) => { + const variant = variantOverride ?? onboardingRHPVariant; + if (variant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE) { Navigation.navigate(ROUTES.HOME); return; } diff --git a/src/components/SidePanel/RHPVariantTest/types.ts b/src/components/SidePanel/RHPVariantTest/types.ts index 7efd441c05264..15dff41a1af68 100644 --- a/src/components/SidePanel/RHPVariantTest/types.ts +++ b/src/components/SidePanel/RHPVariantTest/types.ts @@ -1,4 +1,6 @@ -type ShouldOpenRHPVariant = () => boolean; -type HandleRHPVariantNavigation = (onboardingPolicyID?: string) => void; +import type {OnboardingRHPVariant} from '@src/types/onyx'; + +type ShouldOpenRHPVariant = (variantOverride?: OnboardingRHPVariant | null) => boolean; +type HandleRHPVariantNavigation = (onboardingPolicyID?: string, variantOverride?: OnboardingRHPVariant | null) => void; export type {ShouldOpenRHPVariant, HandleRHPVariantNavigation}; diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index cfb8de469374a..8c7b4b8e0bacd 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -1,5 +1,6 @@ import {handleRHPVariantNavigation, shouldOpenRHPVariant} from '@components/SidePanel/RHPVariantTest'; import ROUTES from '@src/ROUTES'; +import type {OnboardingRHPVariant} from '@src/types/onyx'; import {setDisableDismissOnEscape} from './actions/Modal'; import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom'; import Navigation from './Navigation/Navigation'; @@ -50,11 +51,12 @@ function navigateAfterOnboarding( onboardingPolicyID?: string, onboardingAdminsChatReportID?: string, shouldPreventOpenAdminRoom = false, + onboardingRHPVariant?: OnboardingRHPVariant | null, ) { setDisableDismissOnEscape(false); - if (shouldOpenRHPVariant()) { - handleRHPVariantNavigation(onboardingPolicyID); + if (shouldOpenRHPVariant(onboardingRHPVariant)) { + handleRHPVariantNavigation(onboardingPolicyID, onboardingRHPVariant); return; } @@ -83,6 +85,7 @@ function navigateAfterOnboardingWithMicrotaskQueue( onboardingPolicyID?: string, onboardingAdminsChatReportID?: string, shouldPreventOpenAdminRoom = false, + onboardingRHPVariant?: OnboardingRHPVariant | null, ) { Navigation.dismissModal(); Navigation.setNavigationActionToMicrotaskQueue(() => { @@ -94,6 +97,7 @@ function navigateAfterOnboardingWithMicrotaskQueue( onboardingPolicyID, onboardingAdminsChatReportID, shouldPreventOpenAdminRoom, + onboardingRHPVariant, ); }); } diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index 9429cd1531cca..4cd9edfc256ee 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -33,6 +33,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {OnboardingRHPVariant} from '@src/types/onyx'; import type {BaseOnboardingInterestedFeaturesProps, Feature, SectionObject} from './types'; function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardingInterestedFeaturesProps) { @@ -212,7 +213,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin setOnboardingPolicyID(policyID); } - await completeOnboarding({ + const response = await completeOnboarding({ engagementChoice: onboardingPurposeSelected, onboardingMessage: onboardingMessages[onboardingPurposeSelected], adminsChatReportID, @@ -228,6 +229,11 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin betas, }); + // Extract the RHP variant directly from the API response to avoid a race condition + // where the Onyx callback hasn't fired yet when navigateAfterOnboarding is called. + const rhpVariantUpdate = response?.onyxData?.find((update) => update.key === ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT); + const rhpVariant = rhpVariantUpdate?.value as OnboardingRHPVariant | undefined; + // Avoid creating new WS because onboardingPolicyID is cleared before unmounting // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { @@ -246,6 +252,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin // Onboarding tasks would show in Concierge instead of admins room for testing accounts, we should open where onboarding tasks are located // See https://github.com/Expensify/App/issues/57167 for more details (session?.email ?? '').includes('+'), + rhpVariant, ); } catch (error) { Log.warn('[BaseOnboardingInterestedFeatures] Error completing onboarding', {error}); From 092a5880a0e3e0d39f81f096f6099584242414bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Fri, 13 Mar 2026 16:05:13 -0600 Subject: [PATCH 3/4] Fix typecheck: cast onyxData to broader type for variant extraction The response onyxData is typed with a narrow key union that does not include nvp_onboardingRHPVariant. Cast to Array<{key: string; value: unknown}> to allow searching for the variant key. Co-Authored-By: Claude Opus 4.6 --- .../BaseOnboardingInterestedFeatures.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index 4cd9edfc256ee..2351f302218e1 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -231,7 +231,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin // Extract the RHP variant directly from the API response to avoid a race condition // where the Onyx callback hasn't fired yet when navigateAfterOnboarding is called. - const rhpVariantUpdate = response?.onyxData?.find((update) => update.key === ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT); + const rhpVariantUpdate = (response?.onyxData as Array<{key: string; value: unknown}> | undefined)?.find((update) => update.key === ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT); const rhpVariant = rhpVariantUpdate?.value as OnboardingRHPVariant | undefined; // Avoid creating new WS because onboardingPolicyID is cleared before unmounting From 6830fb4b3cf8d07381e81adec81910d91dc4ba4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Fri, 13 Mar 2026 17:17:23 -0600 Subject: [PATCH 4/4] Open side panel for rhpHomePage variant after navigating to Home The rhpHomePage variant should open the RHP with Concierge DM on the Home page, not just navigate to Home silently. This matches the experiment intent: test whether landing on Home + RHP Concierge performs differently from workspace overview + RHP Concierge. Co-Authored-By: Claude Opus 4.6 --- src/components/SidePanel/RHPVariantTest/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SidePanel/RHPVariantTest/index.ts b/src/components/SidePanel/RHPVariantTest/index.ts index 6cd1ab028ec07..1f6ba8f3393e2 100644 --- a/src/components/SidePanel/RHPVariantTest/index.ts +++ b/src/components/SidePanel/RHPVariantTest/index.ts @@ -49,7 +49,7 @@ const shouldOpenRHPVariant: ShouldOpenRHPVariant = (variantOverride) => { * - Control: navigate to the last accessed report on small screens, do not open side panel * - RHP Concierge DM: navigate to the workspace overview and open the side panel with the Concierge DM * - RHP Admins Room: navigate to the workspace overview and open the side panel with the Admins Room - * - RHP Home Page: navigate to the Home page without opening the side panel or test drive modal + * - RHP Home Page: navigate to the Home page and open the side panel with the Concierge DM * * Accepts an optional variantOverride for the same race-condition reason as shouldOpenRHPVariant. */ @@ -57,6 +57,7 @@ const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicy const variant = variantOverride ?? onboardingRHPVariant; if (variant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE) { Navigation.navigate(ROUTES.HOME); + SidePanelActions.openSidePanel(true); return; }