Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
90181f2
Add onboarding RHP variant type for A/B/C testing
blazejkustra Jan 8, 2026
47a5f50
Introduce SidePanelReport component to display any report.
blazejkustra Jan 8, 2026
886a5b2
Remove unused SidePanelContent type and related content property from…
blazejkustra Jan 9, 2026
615d2de
Add ONBOARDING_RHP_VARIANT constants for new onboarding variants
blazejkustra Jan 9, 2026
24a91f5
Refactor onboarding process to include waitForWrites for write comman…
blazejkustra Jan 9, 2026
2ffa3cd
Handle side panel navigation with specific content.
blazejkustra Jan 9, 2026
c763497
Fix lint
blazejkustra Jan 9, 2026
04bef66
Fix prettier
blazejkustra Jan 9, 2026
b26735a
Refactor SidePanel navigation logic to simplify opening behavior and …
blazejkustra Jan 9, 2026
d2c5721
Check if policy is deleted
blazejkustra Jan 9, 2026
b8e6a54
Update SidePanelContextProvider to use emailSelector for session emai…
blazejkustra Jan 12, 2026
c4a4459
Add COMPLETE_GUIDED_SETUP command and update completeOnboarding logic…
blazejkustra Jan 12, 2026
053d812
Update onboarding feature to include shouldWaitForRHPVariantInitializ…
blazejkustra Jan 12, 2026
5cc2b91
Update waitForWrites function to handle SideEffectRequestCommand and …
blazejkustra Jan 12, 2026
fdd971e
Enhance onboarding navigation logic to restrict RHP variant navigatio…
blazejkustra Jan 12, 2026
fa86fe1
Merge branch 'main' of github.com:Expensify/App into concierge-everyw…
blazejkustra Jan 12, 2026
5d4ac5b
Merge branch 'main' of github.com:Expensify/App into concierge-everyw…
blazejkustra Jan 12, 2026
40e1fe3
Move RHP Variant logic to a separate file and integrate with AdminTes…
blazejkustra Jan 12, 2026
c53d48c
Add RHP Variant logic for native platform with no-op implementations
blazejkustra Jan 13, 2026
042cb72
Add isSidePanelReportSupported logic with type definitions and integr…
blazejkustra Jan 13, 2026
96da54f
Merge branch 'main' of github.com:Expensify/App into concierge-everyw…
blazejkustra Jan 13, 2026
08b8da5
Refactor onboarding logic to include error handling and logging. Adde…
blazejkustra Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5870,6 +5870,11 @@ const CONST = {
ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers},
ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes},
ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize},
ONBOARDING_RHP_VARIANT: {
RHP_CONCIERGE_DM: 'rhpConciergeDm',
RHP_ADMINS_ROOM: 'rhpAdminsRoom',
CONTROL: 'control',
},
ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?',
ONBOARDING_ACCOUNTING_MAPPING,

Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,9 @@ const ONYXKEYS = {
/** Stores the user's app review prompt state and response */
NVP_APP_REVIEW: 'nvp_appReview',

/** Stores the onboarding RHP variant for A/B/C testing */
NVP_ONBOARDING_RHP_VARIANT: 'nvp_onboardingRHPVariant',

/** Information about vacation delegate */
NVP_PRIVATE_VACATION_DELEGATE: 'nvp_private_vacationDelegate',

Expand Down Expand Up @@ -1355,6 +1358,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.BILLING_RECEIPT_DETAILS]: OnyxTypes.BillingReceiptDetails;
[ONYXKEYS.NVP_SIDE_PANEL]: OnyxTypes.SidePanel;
[ONYXKEYS.NVP_APP_REVIEW]: OnyxTypes.AppReview;
[ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT]: OnyxTypes.OnboardingRHPVariant;
[ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION]: boolean;
[ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE]: OnyxTypes.VacationDelegate;
[ONYXKEYS.SCHEDULE_CALL_DRAFT]: OnyxTypes.ScheduleCallDraft;
Expand Down
13 changes: 13 additions & 0 deletions src/components/SidePanel/RHPVariantTest/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types';

/**
* Side Panel is not supported on native platforms, so we always return false.
*/
const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => false;

/**
* No-op on native platforms since Side Panel is not supported.
*/
const handleRHPVariantNavigation: HandleRHPVariantNavigation = () => {};

export {shouldOpenRHPVariant, handleRHPVariantNavigation};
55 changes: 55 additions & 0 deletions src/components/SidePanel/RHPVariantTest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import SidePanelActions from '@libs/actions/SidePanel';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {OnboardingRHPVariant} from '@src/types/onyx';
import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types';

let onboardingRHPVariant: OnyxEntry<OnboardingRHPVariant>;
let onboardingCompanySize: OnyxEntry<string>;

// We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied directly to the UI.
Onyx.connectWithoutView({
key: ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT,
callback: (value) => {
onboardingRHPVariant = value;
},
});

Onyx.connectWithoutView({
Comment on lines +14 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if guys working on Onyx.connect deprecation will mark this as regression

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not use even connectWithoutView. And why is this file in /components folder as all the functions are utils, not connected to UI.

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I would advise against using useOnyx, It's just a beta that will be removed in couple of weeks/months. It's much easier to keep the logic in one place instead of using useOnyx and making a bigger refactor.

The whole project is blocked by this so I would prefer to continue with this approach, wdyt @marcochavezf?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I think that reason would be enough to treat this as not regression.

@marcochavezf marcochavezf Jan 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I agree with @blazejkustra that we should continue with this approach. This is a temporary A/B experiment, and we will update the code again once we have a winning variant (which hopefully can be seen in a couple of weeks)

key: ONYXKEYS.ONBOARDING_COMPANY_SIZE,
callback: (value) => {
onboardingCompanySize = value;
},
});

/**
* 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.
*/
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;

return isMicroCompany && (isRHPConciergeDM || isRHPAdminsRoom);
};

/**
* Handles navigation for RHP experiment:
* - 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
*/
const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID) => {
Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(onboardingPolicyID));
SidePanelActions.openSidePanel(true);
Navigation.isNavigationReady().then(() => {
Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route);
});
};

export {shouldOpenRHPVariant, handleRHPVariantNavigation};
4 changes: 4 additions & 0 deletions src/components/SidePanel/RHPVariantTest/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type ShouldOpenRHPVariant = () => boolean;
type HandleRHPVariantNavigation = (onboardingPolicyID?: string) => void;

export type {ShouldOpenRHPVariant, HandleRHPVariantNavigation};
29 changes: 29 additions & 0 deletions src/components/SidePanel/SidePanelContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import React, {createContext, useCallback, useEffect, useMemo, useRef, useState}
// Import Animated directly from 'react-native' as animations are used with navigation.
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSidePanelDisplayStatus from '@hooks/useSidePanelDisplayStatus';
import useWindowDimensions from '@hooks/useWindowDimensions';
import SidePanelActions from '@libs/actions/SidePanel';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {emailSelector} from '@src/selectors/Session';
import type {SidePanel} from '@src/types/onyx';

type SidePanelContextProps = {
Expand All @@ -25,6 +29,7 @@ type SidePanelContextProps = {
openSidePanel: () => void;
closeSidePanel: () => void;
sidePanelNVP?: SidePanel;
reportID?: string;
};

const SidePanelContext = createContext<SidePanelContextProps>({
Expand Down Expand Up @@ -56,6 +61,28 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
const sidePanelOffset = useRef(new Animated.Value(shouldApplySidePanelOffset ? variables.sidePanelWidth : 0));
const sidePanelTranslateX = useRef(new Animated.Value(shouldHideSidePanel ? sidePanelWidth : 0));

const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
const [onboardingRHPVariant] = useOnyx(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, {canBeMissing: true});
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true});
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {
canBeMissing: true,
selector: emailSelector,
});

const reportID = useMemo(() => {
const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM;
const isUserAdmin = isPolicyAdmin(activePolicy, sessionEmail);
const isPolicyActive = shouldShowPolicy(activePolicy, false, sessionEmail ?? '');
const adminsChatReportID = activePolicy?.chatReportIDAdmins?.toString();

if (isRHPAdminsRoom && isUserAdmin && isPolicyActive && adminsChatReportID) {
return adminsChatReportID;
}

return conciergeReportID;
}, [onboardingRHPVariant, activePolicy, sessionEmail, conciergeReportID]);

useEffect(() => {
setIsSidePanelTransitionEnded(false);
Animated.parallel([
Expand Down Expand Up @@ -103,6 +130,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
openSidePanel: () => SidePanelActions.openSidePanel(!isExtraLargeScreenWidth),
closeSidePanel,
sidePanelNVP,
reportID,
Comment thread
blazejkustra marked this conversation as resolved.
}),
[
closeSidePanel,
Expand All @@ -114,6 +142,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
shouldHideSidePanelBackdrop,
shouldHideToolTip,
sidePanelNVP,
reportID,
],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import {NavigationRouteContext} from '@react-navigation/native';
import React from 'react';
import useOnyx from '@hooks/useOnyx';
import type {ExtraContentProps, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
import ReportScreen from '@pages/home/ReportScreen';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';

const CONCIERGE_REPORT_KEY = 'Report-Concierge-Key';
type SidePanelReportProps = Pick<ExtraContentProps, 'navigation'> & {
reportID: string;
};

function Concierge({navigation}: Pick<ExtraContentProps, 'navigation'>) {
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
function SidePanelReport({navigation, reportID}: SidePanelReportProps) {
// eslint-disable-next-line react/jsx-no-constructed-context-values
const route = !!conciergeReportID && ({name: SCREENS.REPORT, params: {reportID: conciergeReportID}, key: CONCIERGE_REPORT_KEY} as const);

if (!route) {
return null;
}
const route = {name: SCREENS.REPORT, params: {reportID}, key: `Report-SidePanel-${reportID}`} as const;
Comment thread
blazejkustra marked this conversation as resolved.

return (
<NavigationRouteContext.Provider value={route}>
Expand All @@ -29,4 +24,4 @@ function Concierge({navigation}: Pick<ExtraContentProps, 'navigation'>) {
);
}

export default Concierge;
export default SidePanelReport;
11 changes: 7 additions & 4 deletions src/components/SidePanel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import useSidePanel from '@hooks/useSidePanel';
import type {ExtraContentProps} from '@libs/Navigation/PlatformStackNavigation/types';
import Concierge from './Concierge';
import SidePanelModal from './SidePanelModal';
import SidePanelReport from './SidePanelReport';
import useSyncSidePanelWithHistory from './useSyncSidePanelWithHistory';

function SidePanel({navigation}: Pick<ExtraContentProps, 'navigation'>) {
const {sidePanelNVP, isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel} = useSidePanel();
const {sidePanelNVP, isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel, reportID} = useSidePanel();

// Hide side panel once animation ends
// This hook synchronizes the side panel visibility with the browser history when it is displayed as RHP.
Expand All @@ -15,7 +15,7 @@ function SidePanel({navigation}: Pick<ExtraContentProps, 'navigation'>) {
useSyncSidePanelWithHistory();

// Side panel can't be displayed if NVP is undefined
if (!sidePanelNVP) {
if (!sidePanelNVP || !reportID) {
return null;
}

Expand All @@ -30,7 +30,10 @@ function SidePanel({navigation}: Pick<ExtraContentProps, 'navigation'>) {
closeSidePanel={closeSidePanel}
shouldHideSidePanelBackdrop={shouldHideSidePanelBackdrop}
>
<Concierge navigation={navigation} />
<SidePanelReport
navigation={navigation}
reportID={reportID}
/>
</SidePanelModal>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type IsSidePanelReportSupported from './types';

const isSidePanelReportSupported: IsSidePanelReportSupported = false;

export default isSidePanelReportSupported;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type IsSidePanelReportSupported from './types';

const isSidePanelReportSupported: IsSidePanelReportSupported = true;

export default isSidePanelReportSupported;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type IsSidePanelReportSupported = boolean;

export default IsSidePanelReportSupported;
6 changes: 6 additions & 0 deletions src/components/TestDrive/Modal/AdminTestDriveModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {InteractionManager} from 'react-native';
import {shouldOpenRHPVariant} from '@components/SidePanel/RHPVariantTest';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import Log from '@libs/Log';
Expand Down Expand Up @@ -27,6 +28,11 @@ function AdminTestDriveModal() {
Log.hmmm('[AdminTestDriveModal] Skip test drive function called');
Navigation.dismissModal();

if (shouldOpenRHPVariant()) {
Log.hmmm('[AdminTestDriveModal] User was redirected to Workspace Editor, skipping navigation to admin room');
return;
}

Log.hmmm('[AdminTestDriveModal] Running after interactions');
Navigation.setNavigationActionToMicrotaskQueue(() => {
if (!isAdminRoom(onboardingReport)) {
Expand Down
7 changes: 3 additions & 4 deletions src/libs/API/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,10 @@ function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand>(
}

/**
* Ensure all write requests on the sequential queue have finished responding before running read requests.
* Responses from read requests can overwrite the optimistic data inserted by
* write requests that use the same Onyx keys and haven't responded yet.
* Ensure all write requests on the sequential queue have finished responding before running a command.
* Responses from requests can overwrite the optimistic data inserted by write requests that use the same Onyx keys and haven't responded yet.
*/
function waitForWrites<TCommand extends ReadCommand>(command: TCommand) {
function waitForWrites<TCommand extends ReadCommand | WriteCommand | SideEffectRequestCommand>(command: TCommand) {
if (getPersistedRequestsLength() > 0) {
Log.info(`[API] '${command}' is waiting on ${getPersistedRequestsLength()} write commands`);
}
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
CALCULATE_BILL_NEW_DOT: 'CalculateBillNewDot',

ACCEPT_SPOTNANA_TERMS: 'AcceptSpotnanaTerms',
COMPLETE_GUIDED_SETUP: 'CompleteGuidedSetup',
} as const;

type SideEffectRequestCommand = ValueOf<typeof SIDE_EFFECT_REQUEST_COMMANDS>;
Expand Down Expand Up @@ -1312,6 +1313,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.CALCULATE_BILL_NEW_DOT]: null;
[SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: Parameters.AcceptSpotnanaTermsParams;
[SIDE_EFFECT_REQUEST_COMMANDS.GET_SCIM_TOKEN]: Parameters.GetScimTokenParams;
[SIDE_EFFECT_REQUEST_COMMANDS.COMPLETE_GUIDED_SETUP]: Parameters.CompleteGuidedSetupParams;
};

type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters;
Expand Down
Loading
Loading