From 2fd0d9f3a45905fb8134c8d552f52a495b91288b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 16 Dec 2025 20:57:16 +0100 Subject: [PATCH 01/38] Flatten SCREENS.RIGHT_MODAL.SEARCH_REPORT --- .../getVisibleRHPRouteKeys.ts | 1 - .../SecondaryOverlay.tsx | 68 +++++++++++++++++++ .../WideRHPOverlayWrapper/TertiaryOverlay.tsx | 24 +++++++ .../useModalStackScreenOptions.ts | 1 + 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx create mode 100644 src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx diff --git a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts index 6c945a7fa5a35..4d1ff9b4c210f 100644 --- a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts +++ b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts @@ -23,7 +23,6 @@ function getVisibleRHPKeys(allSuperWideRHPKeys: string[], allWideRHPKeys: string } const rootState = navigationRef.getRootState(); - if (!rootState) { return emptyRHPKeysState; } diff --git a/src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx b/src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx new file mode 100644 index 0000000000000..f971db206e199 --- /dev/null +++ b/src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx @@ -0,0 +1,68 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useContext} from 'react'; +import { + animatedReceiptPaneRHPWidth, + modalStackOverlaySuperWideRHPPositionLeft, + modalStackOverlayWideRHPPositionLeft, + secondOverlayRHPOnSuperWideRHPProgress, + secondOverlayRHPOnWideRHPProgress, + secondOverlayWideRHPProgress, + WideRHPContext, +} from '@components/WideRHPContextProvider'; +import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; + +export default function SecondaryOverlay() { + const {shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys, wideRHPRouteKeys} = + useContext(WideRHPContext); + + const route = useRoute(); + + const isWide = !!route?.key && wideRHPRouteKeys.includes(route.key); + const isSuperWide = !!route?.key && superWideRHPRouteKeys.includes(route.key); + + const isRHPDisplayedOnWideRHP = shouldRenderSecondaryOverlayForRHPOnWideRHP && isWide; + const isRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForRHPOnSuperWideRHP && isSuperWide; + const isWideRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForWideRHP && isSuperWide; + + /** + * These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen + * Their position is calculated as follows: + * The width of the window for which we calculate the overlay positions is the width of the RHP window, for example for Super Wide RHP it will be 1260 px on a wide layout. + * We need to move the overlay left from the left edge of the RHP below to the left edge of the RHP above. + * To calculate this, subtract the width of the widest RHP from the width of the RHP above. + * Please note that in these cases, the overlay is rendered from the RHP screen displayed below. For example, if we display RHP on Wide RHP, the secondary overlay is rendered from Wide RHP, etc. + * Three cases were described for the secondary overlay: + * 1. Single RHP is displayed on Wide RHP + * 2. Single RHP is displayed on Super Wide RHP + * 3. Wide RHP is displayed on Super Wide RHP route. + * */ + if (isRHPDisplayedOnWideRHP) { + return ( + + ); + } + + if (isWideRHPDisplayedOnSuperWideRHP) { + return ( + + ); + } + + if (isRHPDisplayedOnSuperWideRHP) { + return ( + + ); + } + + return null; +} diff --git a/src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx b/src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx new file mode 100644 index 0000000000000..7cfae8ff62d16 --- /dev/null +++ b/src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx @@ -0,0 +1,24 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useContext} from 'react'; +import {modalStackOverlaySuperWideRHPPositionLeft, thirdOverlayProgress, WideRHPContext} from '@components/WideRHPContextProvider'; +import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; + +export default function TertiaryOverlay() { + const {shouldRenderTertiaryOverlay, wideRHPRouteKeys} = useContext(WideRHPContext); + const route = useRoute(); + + const isWide = route?.key && wideRHPRouteKeys.includes(route.key); + + // This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen + // There is a special case where three different RHP widths are displayed at the same time. In this case, an overlay under RHP should be rendered from Wide RHP. + if (isWide && shouldRenderTertiaryOverlay) { + return ( + + ); + } + + return null; +} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index 685e72b3fb5a7..7c6a1f4827bed 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -7,6 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import enhanceCardStyleInterpolator from '@libs/Navigation/AppNavigator/enhanceCardStyleInterpolator'; import hideKeyboardOnSwipe from '@libs/Navigation/AppNavigator/hideKeyboardOnSwipe'; import type {PlatformStackNavigationOptions, PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import variables from '@styles/variables'; function useWideModalStackScreenOptions() { const styles = useThemeStyles(); From 9bc71d12c7ab0e6d9196a368a13775ba8ed350d8 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 17 Dec 2025 14:38:53 +0100 Subject: [PATCH 02/38] Move overlays to the wrapper file --- .../SecondaryOverlay.tsx | 68 ------------------- .../WideRHPOverlayWrapper/TertiaryOverlay.tsx | 24 ------- 2 files changed, 92 deletions(-) delete mode 100644 src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx delete mode 100644 src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx diff --git a/src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx b/src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx deleted file mode 100644 index f971db206e199..0000000000000 --- a/src/components/WideRHPOverlayWrapper/SecondaryOverlay.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useContext} from 'react'; -import { - animatedReceiptPaneRHPWidth, - modalStackOverlaySuperWideRHPPositionLeft, - modalStackOverlayWideRHPPositionLeft, - secondOverlayRHPOnSuperWideRHPProgress, - secondOverlayRHPOnWideRHPProgress, - secondOverlayWideRHPProgress, - WideRHPContext, -} from '@components/WideRHPContextProvider'; -import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; - -export default function SecondaryOverlay() { - const {shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys, wideRHPRouteKeys} = - useContext(WideRHPContext); - - const route = useRoute(); - - const isWide = !!route?.key && wideRHPRouteKeys.includes(route.key); - const isSuperWide = !!route?.key && superWideRHPRouteKeys.includes(route.key); - - const isRHPDisplayedOnWideRHP = shouldRenderSecondaryOverlayForRHPOnWideRHP && isWide; - const isRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForRHPOnSuperWideRHP && isSuperWide; - const isWideRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForWideRHP && isSuperWide; - - /** - * These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen - * Their position is calculated as follows: - * The width of the window for which we calculate the overlay positions is the width of the RHP window, for example for Super Wide RHP it will be 1260 px on a wide layout. - * We need to move the overlay left from the left edge of the RHP below to the left edge of the RHP above. - * To calculate this, subtract the width of the widest RHP from the width of the RHP above. - * Please note that in these cases, the overlay is rendered from the RHP screen displayed below. For example, if we display RHP on Wide RHP, the secondary overlay is rendered from Wide RHP, etc. - * Three cases were described for the secondary overlay: - * 1. Single RHP is displayed on Wide RHP - * 2. Single RHP is displayed on Super Wide RHP - * 3. Wide RHP is displayed on Super Wide RHP route. - * */ - if (isRHPDisplayedOnWideRHP) { - return ( - - ); - } - - if (isWideRHPDisplayedOnSuperWideRHP) { - return ( - - ); - } - - if (isRHPDisplayedOnSuperWideRHP) { - return ( - - ); - } - - return null; -} diff --git a/src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx b/src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx deleted file mode 100644 index 7cfae8ff62d16..0000000000000 --- a/src/components/WideRHPOverlayWrapper/TertiaryOverlay.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useContext} from 'react'; -import {modalStackOverlaySuperWideRHPPositionLeft, thirdOverlayProgress, WideRHPContext} from '@components/WideRHPContextProvider'; -import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; - -export default function TertiaryOverlay() { - const {shouldRenderTertiaryOverlay, wideRHPRouteKeys} = useContext(WideRHPContext); - const route = useRoute(); - - const isWide = route?.key && wideRHPRouteKeys.includes(route.key); - - // This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen - // There is a special case where three different RHP widths are displayed at the same time. In this case, an overlay under RHP should be rendered from Wide RHP. - if (isWide && shouldRenderTertiaryOverlay) { - return ( - - ); - } - - return null; -} From 0d7768d992668a2279b91c42e31fc314dd39f309 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Mon, 15 Dec 2025 13:23:57 +0100 Subject: [PATCH 03/38] Reapply "Merge pull request #75886 from software-mansion-labs/swrhp-v2" This reverts commit e386d63ddb8e472a70802d419d859ceae43f6d59. --- src/ROUTES.ts | 11 ++ src/SCREENS.ts | 4 +- src/components/MoneyReportHeader.tsx | 18 +-- .../MoneyRequestReportActionsList.tsx | 4 +- .../MoneyRequestReportTransactionItem.tsx | 4 +- .../MoneyRequestReportTransactionList.tsx | 4 +- .../MoneyRequestReportView.tsx | 114 ++++++++++++------ src/components/ParentNavigationSubtitle.tsx | 59 ++++++++- .../MoneyRequestReceiptView.tsx | 3 +- .../MoneyRequestReportPreviewContent.tsx | 14 ++- .../MoneyRequestReportPreview/index.tsx | 13 +- src/components/Search/index.tsx | 19 ++- .../SelectionList/ListItem/SplitListItem.tsx | 18 ++- .../ReceiptPreview/index.tsx | 4 +- .../getVisibleRHPRouteKeys.ts | 7 +- .../index.native.ts | 13 ++ .../useResponsiveLayoutOnWideRHP/index.ts | 33 +++++ .../useResponsiveLayoutOnWideRHP/types.ts | 8 ++ .../ModalStackNavigators/index.tsx | 13 ++ .../Navigators/RightModalNavigator.tsx | 8 ++ .../Navigators/SearchFullscreenNavigator.tsx | 5 - src/libs/Navigation/Navigation.ts | 68 +++++++++-- .../Navigation/helpers/isReportOpenInRHP.ts | 3 +- .../helpers/isReportOpenInSuperWideRHP.ts | 17 +++ .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 13 +- src/libs/Navigation/types.ts | 25 +++- src/libs/ReportUtils.ts | 34 +++--- src/libs/actions/IOU/index.ts | 16 ++- src/pages/ReportAddApproverPage.tsx | 2 +- src/pages/ReportChangeApproverPage.tsx | 4 +- .../Search/SearchMoneyRequestReportPage.tsx | 71 ++++++----- .../TransactionMerge/ConfirmationPage.tsx | 2 +- src/pages/iou/SplitExpensePage.tsx | 6 +- .../iou/request/step/IOURequestStepReport.tsx | 4 +- src/styles/index.ts | 2 +- src/styles/variables.ts | 1 + tests/actions/IOUTest.ts | 2 + .../ReportActionCompose.perf-test.tsx | 1 + tests/utils/TestNavigationContainer.tsx | 4 - 40 files changed, 493 insertions(+), 159 deletions(-) create mode 100644 src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts create mode 100644 src/hooks/useResponsiveLayoutOnWideRHP/index.ts create mode 100644 src/hooks/useResponsiveLayoutOnWideRHP/types.ts create mode 100644 src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 16c126be5dfe8..e272d8309e223 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -91,6 +91,17 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + + EXPENSE_REPORT_RHP: { + route: 'e/:reportID', + getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { + const baseRoute = `e/${reportID}` as const; + + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + return getUrlWithBackToParam(baseRoute, backTo); + }, + }, + SEARCH_REPORT_VERIFY_ACCOUNT: { route: `search/view/:reportID/${VERIFY_ACCOUNT}`, getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 989a9ae3fe92d..b43b245fafe55 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -255,8 +255,6 @@ const SCREENS = { TRAVEL: 'Travel', SEARCH_REPORT: 'SearchReport', SEARCH_REPORT_ACTIONS: 'SearchReportActions', - // These two routes will be added in a separate PR adding Super Wide RHP routes - EXPENSE_REPORT: 'ExpenseReport', SEARCH_MONEY_REQUEST_REPORT: 'SearchMoneyRequestReport', SEARCH_COLUMNS: 'SearchColumns', @@ -277,8 +275,10 @@ const SCREENS = { MERGE_TRANSACTION: 'MergeTransaction', REPORT_CARD_ACTIVATE: 'Report_Card_Activate', DOMAIN: 'Domain', + EXPENSE_REPORT: 'ExpenseReport', }, REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root', + EXPENSE_REPORT_RHP: 'Expense_Report_RHP', PUBLIC_CONSOLE_DEBUG: 'Console_Debug', SAML_SIGN_IN: 'SAMLSignIn', WORKSPACE_JOIN_USER: 'WorkspaceJoinUser', diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 11a7328031ae0..5725a4794dab3 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -24,6 +24,7 @@ import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; import useStrictPolicyRules from '@hooks/useStrictPolicyRules'; @@ -45,7 +46,7 @@ import Log from '@libs/Log'; import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList, SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList, SearchMoneyRequestReportParamList} from '@libs/Navigation/types'; import { buildOptimisticNextStepForDEWOfflineSubmission, buildOptimisticNextStepForDynamicExternalWorkflowError, @@ -156,7 +157,6 @@ import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import {useSearchContext} from './Search/SearchContext'; import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton'; import Text from './Text'; -import {WideRHPContext} from './WideRHPContextProvider'; type MoneyReportHeaderProps = { /** The report currently being looked at */ @@ -197,7 +197,7 @@ function MoneyReportHeader({ const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth; const route = useRoute< | PlatformStackRouteProp - | PlatformStackRouteProp + | PlatformStackRouteProp | PlatformStackRouteProp >(); const {login: currentUserLogin, accountID, email} = useCurrentUserPersonalDetails(); @@ -402,9 +402,11 @@ function MoneyReportHeader({ const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.similarSearchHash, true); const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON?.hash}`, {canBeMissing: true}); - const {wideRHPRouteKeys} = useContext(WideRHPContext); const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true}); - const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || (wideRHPRouteKeys.length > 0 && !isSmallScreenWidth); + + const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP(); + + const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout; const showExportProgressModal = useCallback(() => { return showConfirmModal({ @@ -1549,7 +1551,9 @@ function MoneyReportHeader({ const backToRoute = route.params?.backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined); Navigation.goBack(backToRoute); } - handleDeleteTransactions(); + // It has been handled like the rest of the delete cases. It will be refactored along with other cases. + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => handleDeleteTransactions()); }); }, [showConfirmModal, translate, selectedTransactionIDs.length, transactions, handleDeleteTransactions, route.params?.backTo, chatReport?.reportID]); @@ -1731,7 +1735,7 @@ function MoneyReportHeader({ {isReportInSearch && ( )} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 7de13fef2bfb7..df2e7d475a595 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -30,7 +30,7 @@ import useParentReportAction from '@hooks/useParentReportAction'; import usePrevious from '@hooks/usePrevious'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useReportScrollManager from '@hooks/useReportScrollManager'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -172,7 +172,7 @@ function MoneyRequestReportActionsList({ const isReportArchived = useReportIsArchived(reportID); const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`, {canBeMissing: true}); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx index badcc79500450..349db531a5816 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx @@ -8,6 +8,7 @@ import TransactionItemRow from '@components/TransactionItemRow'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; @@ -81,7 +82,8 @@ function MoneyRequestReportTransactionItem({ const {translate} = useLocalize(); const styles = useThemeStyles(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth, isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const theme = useTheme(); const isPendingDelete = isTransactionPendingDelete(transaction); const pendingAction = getTransactionPendingAction(transaction); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 5f8d6ac0eaa99..4a9598349e7cb 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -21,6 +21,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -168,7 +169,8 @@ function MoneyRequestReportTransactionList({ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'CheckSquare', 'ReceiptPlus']); const {translate, localeCompare} = useLocalize(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const {markReportIDAsExpense} = useContext(WideRHPContext); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedTransactionID, setSelectedTransactionID] = useState(''); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 1d821678276d8..c5e2b35a56379 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -1,10 +1,14 @@ import {PortalHost} from '@gorhom/portal'; import React, {useCallback, useEffect, useMemo} from 'react'; -import {InteractionManager, View} from 'react-native'; +// We use Animated for all functionality related to wide RHP to make it easier +// to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. +// eslint-disable-next-line no-restricted-imports +import {Animated, InteractionManager, ScrollView, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import MoneyRequestReceiptView from '@components/ReportActionItem/MoneyRequestReceiptView'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import useNetwork from '@hooks/useNetwork'; @@ -12,6 +16,7 @@ import useNewTransactions from '@hooks/useNewTransactions'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useParentReportAction from '@hooks/useParentReportAction'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {removeFailedReport} from '@libs/actions/Report'; @@ -56,6 +61,16 @@ function goBackFromSearchMoneyRequest() { const rootState = navigationRef.getRootState(); const lastRoute = rootState.routes.at(-1); + if (!lastRoute) { + Log.hmmm('[goBackFromSearchMoneyRequest()] No last route found in root state.'); + return; + } + + if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + Navigation.goBack(); + return; + } + if (lastRoute?.name !== NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { Log.hmmm('[goBackFromSearchMoneyRequest()] goBackFromSearchMoneyRequest was called from a different navigator than SearchFullscreenNavigator.'); return; @@ -84,6 +99,10 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe const styles = useThemeStyles(); const {isOffline} = useNetwork(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const reportID = report?.reportID; const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {canBeMissing: true}); @@ -129,6 +148,9 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe const isEmptyTransactionReport = visibleTransactions && visibleTransactions.length === 0 && transactionThreadReportID === undefined; const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? []); + const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true}); + const shouldShowWideRHPReceipt = visibleTransactions.length === 1 && !isSmallScreenWidth && !!transactionThreadReport; + const reportHeaderView = useMemo( () => isTransactionThreadView ? ( @@ -223,48 +245,62 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe contentContainerStyle={styles.flex1} errorRowStyles={[styles.ph5, styles.mv2]} > - - {shouldDisplayMoneyRequestActionsList ? ( - - ) : ( - + + {shouldShowWideRHPReceipt && ( + + + + + )} - {shouldDisplayReportFooter ? ( - <> - + {shouldDisplayMoneyRequestActionsList ? ( + + ) : ( + - - - ) : null} + )} + {shouldDisplayReportFooter ? ( + <> + + + + ) : null} + diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 301b09e06ceba..8a3fb2d3a37d4 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -12,7 +12,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; -import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {SearchMoneyRequestReportParamList} from '@libs/Navigation/types'; import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -91,8 +91,23 @@ function ParentNavigationSubtitle({ const isReportArchived = useReportIsArchived(report?.reportID); const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived); const isReportInRHP = currentRoute.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; - const currentFullScreenRoute = useRootNavigationState((state) => state?.routes?.findLast((route) => isFullScreenName(route.name))); const hasAccessToParentReport = currentReport?.hasParentAccess !== false; + const {currentFullScreenRoute, currentFocusedNavigator} = useRootNavigationState((state) => { + const fullScreenRoute = state?.routes?.findLast((route) => isFullScreenName(route.name)); + + // We need to track which navigator is focused to handle parent report navigation correctly: + // if we are in RHP, and parent report is opened in RHP, we want to go back to the parent report + const focusedNavigator = state?.routes + ? state.routes.findLast((route) => { + return route.state?.routes && route.state.routes.length > 0; + }) + : undefined; + + return { + currentFullScreenRoute: fullScreenRoute, + currentFocusedNavigator: focusedNavigator, + }; + }); // We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case) if (!reportName) { @@ -108,7 +123,7 @@ function ParentNavigationSubtitle({ if (currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR && isMoneyRequestReport(report)) { const lastRoute = currentFullScreenRoute?.state?.routes.at(-1); if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - const moneyRequestReportID = (lastRoute?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID; + const moneyRequestReportID = (lastRoute?.params as SearchMoneyRequestReportParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID; // If the parent report is already displayed underneath RHP, simply dismiss the modal if (moneyRequestReportID === parentReportID) { Navigation.dismissModal(); @@ -116,6 +131,23 @@ function ParentNavigationSubtitle({ } } + const focusedNavigatorState = currentFocusedNavigator?.state; + const currentReportIndex = focusedNavigatorState?.index; + // Dismiss wide RHP and go back to already opened super wide RHP if the parent report is already opened there + if (currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && currentReportIndex && currentReportIndex > 0) { + const previousRoute = focusedNavigatorState.routes[currentReportIndex - 1]; + + if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) { + const lastPreviousRoute = previousRoute.state?.routes.at(-1); + const moneyRequestReportID = (lastPreviousRoute?.params as SearchMoneyRequestReportParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID; + + if (moneyRequestReportID === parentReportID && lastPreviousRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { + Navigation.goBack(); + return; + } + } + } + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: parentReportID})); return; } @@ -129,12 +161,27 @@ function ParentNavigationSubtitle({ // When viewing a money request in the search navigator, open the parent report in a right-hand pane (RHP) // to preserve the search context instead of navigating away. - if (openParentReportInCurrentTab && currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { - const lastRoute = currentFullScreenRoute?.state?.routes.at(-1); - if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { + if (openParentReportInCurrentTab && currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + const lastRoute = currentFocusedNavigator?.state?.routes.at(-1); + if (lastRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) { Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID, reportActionID: parentReportActionID})); return; } + + // Specific case: when opening expense report from search report (chat RHP), + // avoid stacking RHPs by going back to the search report if it's already there + const previousRoute = currentFocusedNavigator?.state?.routes.at(-2); + + if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + if (previousRoute.params && 'reportID' in previousRoute.params) { + const reportIDFromParams = previousRoute.params.reportID; + + if (reportIDFromParams === parentReportID) { + Navigation.goBack(); + return; + } + } + } } if (isVisibleAction) { diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index f8d4347c71ac9..9ea62f0f8ea3d 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -159,7 +159,8 @@ function MoneyRequestReceiptView({ const transactionToCheck = updatedTransaction ?? transaction; const doesTransactionHaveReceipt = !!transactionToCheck?.receipt && !isEmptyObject(transactionToCheck?.receipt); - const shouldShowReceiptEmptyState = !isInvoice && !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt; + // @TODO: Verify if we don't need to check isInvoice here + const shouldShowReceiptEmptyState = !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt; const [receiptImageViolations, receiptViolations] = useMemo(() => { const imageViolations = []; diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index c2819921fd91c..504981d3488f2 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -143,7 +143,8 @@ function MoneyRequestReportPreviewContent({ const StyleUtils = useStyleUtils(); const {translate, formatPhoneNumber} = useLocalize(); const {isOffline} = useNetwork(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const currentUserDetails = useCurrentUserPersonalDetails(); const currentUserAccountID = currentUserDetails.accountID; const currentUserEmail = currentUserDetails.email ?? ''; @@ -517,9 +518,14 @@ function MoneyRequestReportPreviewContent({ name: 'MoneyRequestReportPreviewContent', op: CONST.TELEMETRY.SPAN_OPEN_REPORT, }); - - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); - }, [iouReportID]); + // Small screens navigate to full report view since super wide RHP + // is not available on narrow layouts and would break the navigation logic. + if (isSmallScreenWidth) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); + } else { + Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()})); + } + }, [iouReportID, isSmallScreenWidth]); const isDEWPolicy = hasDynamicExternalWorkflow(policy); const isDEWSubmitPending = hasPendingDEWSubmit(iouReportMetadata, isDEWPolicy); diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx index 3de2093e0fb32..0ddd856501be4 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx @@ -41,7 +41,8 @@ function MoneyRequestReportPreview({ }: MoneyRequestReportPreviewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const personalDetailsList = usePersonalDetails(); const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]; const invoiceReceiverPolicy = @@ -110,8 +111,14 @@ function MoneyRequestReportPreview({ name: 'MoneyRequestReportPreview', op: CONST.TELEMETRY.SPAN_OPEN_REPORT, }); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); - }, [iouReportID]); + // Small screens navigate to full report view since super wide RHP + // is not available on narrow layouts and would break the navigation logic. + if (isSmallScreenWidth) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); + } else { + Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()})); + } + }, [iouReportID, isSmallScreenWidth]); const renderItem: ListRenderItem = ({item}) => ( 1) { + markReportIDAsMultiTransactionExpense(reportID); + } else { + unmarkReportIDAsMultiTransactionExpense(reportID); + } + requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID, backTo}))); return; } @@ -840,7 +848,16 @@ function Search({ requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo}))); }, - [isMobileSelectionModeEnabled, toggleTransaction, queryJSON, handleSearch, searchKey, markReportIDAsExpense], + [ + isMobileSelectionModeEnabled, + markReportIDAsExpense, + toggleTransaction, + queryJSON, + handleSearch, + searchKey, + markReportIDAsMultiTransactionExpense, + unmarkReportIDAsMultiTransactionExpense, + ], ); const currentColumns = useMemo(() => { diff --git a/src/components/SelectionList/ListItem/SplitListItem.tsx b/src/components/SelectionList/ListItem/SplitListItem.tsx index fbf5215d7869d..548c4694b7cd6 100644 --- a/src/components/SelectionList/ListItem/SplitListItem.tsx +++ b/src/components/SelectionList/ListItem/SplitListItem.tsx @@ -1,5 +1,5 @@ -import React, {useCallback, useLayoutEffect, useRef, useState} from 'react'; -import {View} from 'react-native'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; import Icon from '@components/Icon'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; @@ -63,13 +63,19 @@ function SplitListItem({ }, [onInputFocus, item]); // Auto-focus input when item is selected and screen transition ends - useLayoutEffect(() => { - if (!splitItem.isSelected || !splitItem.isEditable || !didScreenTransitionEnd || !inputRef.current) { + useEffect(() => { + if (!didScreenTransitionEnd || !splitItem.isSelected || !splitItem.isEditable || !inputRef.current) { return; } - inputRef.current.focus(); - }, [splitItem.isSelected, splitItem.isEditable, didScreenTransitionEnd]); + // Use InteractionManager to ensure input focus happens after all animations/interactions complete. + // This prevents focus from interrupting modal close/open animations which would cause UI glitches + // and "jumping" behavior when quickly navigating between screens. + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + inputRef.current?.focus(); + }); + }, [didScreenTransitionEnd, splitItem.isSelected, splitItem.isEditable]); const inputCallbackRef = (ref: BaseTextInputRef | null) => { inputRef.current = ref; diff --git a/src/components/TransactionItemRow/ReceiptPreview/index.tsx b/src/components/TransactionItemRow/ReceiptPreview/index.tsx index be3c27af98846..9a7da1e77ebaa 100644 --- a/src/components/TransactionItemRow/ReceiptPreview/index.tsx +++ b/src/components/TransactionItemRow/ReceiptPreview/index.tsx @@ -8,7 +8,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation'; import type {ImageOnLoadEvent} from '@components/Image/types'; import useDebouncedState from '@hooks/useDebouncedState'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {hasReceiptSource, isDistanceRequest, isManualDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; @@ -40,7 +40,7 @@ function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}: const [imageAspectRatio, setImageAspectRatio] = useState(undefined); const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState(undefined); const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const hasMeasured = useRef(false); const {windowHeight} = useWindowDimensions(); const [isLoading, setIsLoading] = useState(true); diff --git a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts index 4d1ff9b4c210f..416c5baa02298 100644 --- a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts +++ b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts @@ -34,7 +34,12 @@ function getVisibleRHPKeys(allSuperWideRHPKeys: string[], allWideRHPKeys: string return emptyRHPKeysState; } - const superWideRHPIndex = lastRHPRoute.state?.routes.findLastIndex((route) => route?.key && allSuperWideRHPKeys.includes(route.key)) ?? -1; + const superWideRHPIndex = + lastRHPRoute.state?.routes.findLastIndex((route) => { + const focusedRouteKey = route.state?.routes?.at(0)?.key; + return focusedRouteKey && allSuperWideRHPKeys.includes(focusedRouteKey); + }) ?? -1; + const wideRHPIndex = lastRHPRoute.state?.routes.findLastIndex((route) => route?.key && allWideRHPKeys.includes(route.key)) ?? -1; let visibleRHPKeys; diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts b/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts new file mode 100644 index 0000000000000..505b966964c19 --- /dev/null +++ b/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts @@ -0,0 +1,13 @@ +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import type ResponsiveLayoutOnWideRHPResult from './types'; + +// Super Wide and Wide RHPs are not displayed on native platforms. +export default function useResponsiveLayoutOnWideRHP(): ResponsiveLayoutOnWideRHPResult { + const responsiveLayoutValues = useResponsiveLayout(); + + return { + ...responsiveLayoutValues, + isWideRHPDisplayedOnWideLayout: false, + isSuperWideRHPDisplayedOnWideLayout: false, + }; +} diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/index.ts b/src/hooks/useResponsiveLayoutOnWideRHP/index.ts new file mode 100644 index 0000000000000..8919d15bdfe60 --- /dev/null +++ b/src/hooks/useResponsiveLayoutOnWideRHP/index.ts @@ -0,0 +1,33 @@ +import {useRoute} from '@react-navigation/native'; +import {useContext} from 'react'; +import {WideRHPContext} from '@components/WideRHPContextProvider'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import type ResponsiveLayoutOnWideRHPResult from './types'; + +/** + * useResponsiveLayoutOnWideRHP is a wrapper on useResponsiveLayout. shouldUseNarrowLayout on a wide screen is true when the screen is displayed in RHP. + * In this hook this value is modified when the screen is displayed in Wide/Super Wide RHP, then in wide screen this value is false. + */ +export default function useResponsiveLayoutOnWideRHP(): ResponsiveLayoutOnWideRHPResult { + const route = useRoute(); + + const responsiveLayoutValues = useResponsiveLayout(); + + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth, isInNarrowPaneModal} = responsiveLayoutValues; + + const {superWideRHPRouteKeys, wideRHPRouteKeys} = useContext(WideRHPContext); + + const isWideRHPDisplayedOnWideLayout = !isSmallScreenWidth && wideRHPRouteKeys.includes(route?.key); + + const isSuperWideRHPDisplayedOnWideLayout = !isSmallScreenWidth && superWideRHPRouteKeys.includes(route?.key); + + const shouldUseNarrowLayout = (isSmallScreenWidth || isInNarrowPaneModal) && !isSuperWideRHPDisplayedOnWideLayout && !isWideRHPDisplayedOnWideLayout; + + return { + ...responsiveLayoutValues, + shouldUseNarrowLayout, + isWideRHPDisplayedOnWideLayout, + isSuperWideRHPDisplayedOnWideLayout, + }; +} diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/types.ts b/src/hooks/useResponsiveLayoutOnWideRHP/types.ts new file mode 100644 index 0000000000000..ca889fb4b263d --- /dev/null +++ b/src/hooks/useResponsiveLayoutOnWideRHP/types.ts @@ -0,0 +1,8 @@ +import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; + +type ResponsiveLayoutOnWideRHPResult = ResponsiveLayoutResult & { + isWideRHPDisplayedOnWideLayout: boolean; + isSuperWideRHPDisplayedOnWideLayout: boolean; +}; + +export default ResponsiveLayoutOnWideRHPResult; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8e70d36864994..0c7e87b0657fa 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -13,6 +13,7 @@ import type { DebugParamList, EditRequestNavigatorParamList, EnablePaymentsNavigatorParamList, + ExpenseReportNavigatorParamList, FlagCommentNavigatorParamList, MergeTransactionNavigatorParamList, MissingPersonalDetailsParamList, @@ -36,6 +37,7 @@ import type { ScheduleCallParamList, SearchAdvancedFiltersParamList, SearchColumnsParamList, + SearchMoneyRequestReportParamList, SearchReportActionsParamList, SearchSavedSearchParamList, SettingsNavigatorParamList, @@ -883,6 +885,11 @@ const SearchReportActionsModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchTransactionsChangeReport').default, }); +// This navigator is reserved for the screen that can be displayed as Super Wide RHP, other screens should not be added here. +const SearchMoneyRequestReportModalStackNavigator = createModalStackNavigator({ + [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: () => require('../../../../pages/Search/SearchMoneyRequestReportPage').default, +}); + const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator({ [SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTypePage').default, @@ -970,6 +977,10 @@ const ScheduleCallModalStackNavigator = createModalStackNavigator require('../../../../pages/ScheduleCall/ScheduleCallConfirmationPage').default, }); +const ExpenseReportModalStackNavigator = createModalStackNavigator({ + [SCREENS.EXPENSE_REPORT_RHP]: () => require('../../../../pages/Search/SearchMoneyRequestReportPage').default, +}); + const WorkspacesDomainModalStackNavigator = createModalStackNavigator({ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/WorkspacesVerifyDomainPage').default, [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/WorkspacesDomainVerifiedPage').default, @@ -1028,4 +1039,6 @@ export { WorkspaceConfirmationModalStackNavigator, WorkspaceDuplicateModalStackNavigator, WorkspacesDomainModalStackNavigator, + SearchMoneyRequestReportModalStackNavigator, + ExpenseReportModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 52d83ca526f64..1e4f5f9c9c1ba 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -351,6 +351,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { return {...options, animation: animationEnabledOnSearchReport ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; }} /> + + require('@pages/Search/SearchPage').default; -const loadSearchMoneyReportPage = () => require('@pages/Search/SearchMoneyRequestReportPage').default; const Stack = createSearchFullscreenNavigator(); @@ -36,10 +35,6 @@ function SearchFullscreenNavigator({route}: PlatformStackScreenProps - ); diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 9f0bf092e5c60..71f0ad20cbae8 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -37,7 +37,17 @@ import setNavigationActionToMicrotaskQueue from './helpers/setNavigationActionTo import {linkingConfig} from './linkingConfig'; import {SPLIT_TO_SIDEBAR} from './linkingConfig/RELATIONS'; import navigationRef from './navigationRef'; -import type {NavigationPartialRoute, NavigationRef, NavigationRoute, NavigationStateRoute, ReportsSplitNavigatorParamList, RootNavigatorParamList, State} from './types'; +import type { + ExpenseReportNavigatorParamList, + NavigationPartialRoute, + NavigationRef, + NavigationRoute, + NavigationStateRoute, + ReportsSplitNavigatorParamList, + RootNavigatorParamList, + SearchMoneyRequestReportParamList, + State, +} from './types'; // Routes which are part of the flow to set up 2FA const SET_UP_2FA_ROUTES = new Set([ @@ -227,15 +237,20 @@ function navigate(route: Route, options?: LinkToOptions) { } // Start a Sentry span for report navigation - if (route.startsWith('r/') || route.startsWith('search/r/')) { + if (route.startsWith('r/') || route.startsWith('search/r/') || route.startsWith('e/')) { const routePath = Str.cutAfter(route, '?'); - const reportIDMatch = routePath.match(/^(?:search\/)?r\/(\d+)(?:\/\d+)?$/); + const reportIDMatch = route.match(/^(?:search\/)?(?:r|e)\/(\w+)/); const reportID = reportIDMatch?.at(1); if (reportID) { const spanId = `${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`; let span = getSpan(spanId); if (!span) { - const spanName = route.startsWith('r/') ? '/r/*' : '/search/r/*'; + let spanName = '/r/*'; + if (route.startsWith('search/r/')) { + spanName = '/search/r/*'; + } else if (route.startsWith('e/')) { + spanName = '/e/*'; + } span = startSpan(spanId, { name: spanName, op: CONST.TELEMETRY.SPAN_OPEN_REPORT, @@ -248,11 +263,9 @@ function navigate(route: Route, options?: LinkToOptions) { }); } } - linkTo(navigationRef.current, route, options); closeSidePanelOnNarrowScreen(); } - /** * When routes are compared to determine whether the fallback route passed to the goUp function is in the state, * these parameters shouldn't be included in the comparison. @@ -596,6 +609,37 @@ function getReportRouteByID(reportID?: string, routes: NavigationRoute[] = navig return null; } +/** + * Get the report ID from the topmost Super Wide RHP modal in the navigation stack. + */ +function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.getRootState()): string | undefined { + if (!state) { + return; + } + + const topmostRightModalNavigator = state.routes?.at(-1); + + if (!topmostRightModalNavigator || topmostRightModalNavigator.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + return; + } + + const topmostSuperWideRHPModalStack = topmostRightModalNavigator.state?.routes.findLast((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name)); + + if (!topmostSuperWideRHPModalStack) { + return; + } + + const topmostSuperWideRHP = topmostSuperWideRHPModalStack.state?.routes.findLast( + (route) => route.name === SCREENS.EXPENSE_REPORT_RHP || route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT, + ); + const topmostReportParams = topmostSuperWideRHP?.params as + | SearchMoneyRequestReportParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT] + | ExpenseReportNavigatorParamList[typeof SCREENS.EXPENSE_REPORT_RHP] + | undefined; + + return topmostReportParams?.reportID; +} + /** * Closes the modal navigator (RHP, onboarding). * @@ -627,8 +671,16 @@ const dismissModal = ({ref = navigationRef, callback}: {ref?: NavigationRef; cal */ const dismissModalWithReport = ({reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef) => { isNavigationReady().then(() => { + const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); + let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; + + if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { + dismissToPreviousRHP(); + return; + } + const topmostReportID = getTopmostReportId(); - const areReportsIDsDefined = !!topmostReportID && !!reportID; + areReportsIDsDefined = !!topmostReportID && !!reportID; const isReportsSplitTopmostFullScreen = ref.getRootState().routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { dismissModal(); @@ -752,7 +804,7 @@ function dismissToModalStack(modalStackNames: Set) { return; } - const lastFoundModalStackIndex = rhpState.routes.findLastIndex((route) => modalStackNames.has(route.name)); + const lastFoundModalStackIndex = rhpState.routes.slice(0, -1).findLastIndex((route) => modalStackNames.has(route.name)); const routesToPop = rhpState.routes.length - lastFoundModalStackIndex - 1; if (routesToPop <= 0 || lastFoundModalStackIndex === -1) { diff --git a/src/libs/Navigation/helpers/isReportOpenInRHP.ts b/src/libs/Navigation/helpers/isReportOpenInRHP.ts index 51e8a95bb66bd..fde83327e71bd 100644 --- a/src/libs/Navigation/helpers/isReportOpenInRHP.ts +++ b/src/libs/Navigation/helpers/isReportOpenInRHP.ts @@ -1,4 +1,5 @@ import type {NavigationState} from '@react-navigation/native'; +import {ALL_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; @@ -11,7 +12,7 @@ const isReportOpenInRHP = (state: NavigationState | undefined): boolean => { if (params && 'screen' in params && typeof params.screen === 'string' && params.screen === SCREENS.RIGHT_MODAL.SEARCH_REPORT) { return true; } - return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => route?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT)); + return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => ALL_WIDE_RIGHT_MODALS.has(route.name))); }; export default isReportOpenInRHP; diff --git a/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts b/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts new file mode 100644 index 0000000000000..71084ffcdb126 --- /dev/null +++ b/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts @@ -0,0 +1,17 @@ +import type {NavigationState} from '@react-navigation/native'; +import {SUPER_WIDE_RIGHT_MODALS, WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; +import NAVIGATORS from '@src/NAVIGATORS'; + +const isReportOpenInSuperWideRHP = (state: NavigationState | undefined): boolean => { + const lastRoute = state?.routes?.at(-1); + if (!lastRoute) { + return false; + } + const params = lastRoute.params; + if (params && 'screen' in params && typeof params.screen === 'string' && WIDE_RIGHT_MODALS.has(params.screen)) { + return true; + } + return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name))); +}; + +export default isReportOpenInSuperWideRHP; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index b6ba33cb8c56f..66cdc5794eedc 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -51,6 +51,7 @@ const SEARCH_TO_RHP: Partial['config'] = { [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP, }, }, + [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: { + screens: { + [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT.route, + }, + }, [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: { screens: { [SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(), @@ -1870,6 +1875,11 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, }, }, + [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: { + screens: { + [SCREENS.EXPENSE_REPORT_RHP]: ROUTES.EXPENSE_REPORT_RHP.route, + }, + }, [SCREENS.RIGHT_MODAL.DOMAIN]: { screens: { [SCREENS.WORKSPACES_VERIFY_DOMAIN]: { @@ -2035,9 +2045,6 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ROOT]: { path: ROUTES.SEARCH_ROOT.route, }, - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: { - path: ROUTES.SEARCH_MONEY_REQUEST_REPORT.route, - }, }, }, [NAVIGATORS.SHARE_MODAL_NAVIGATOR]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7854b8717bcd2..ce69cedf1b3a8 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2241,6 +2241,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TRAVEL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: NavigatorScreenParams; @@ -2252,6 +2253,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { @@ -2782,6 +2784,14 @@ type SearchReportActionsParamList = { }; }; +type SearchMoneyRequestReportParamList = { + [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: { + reportID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; +}; + type SearchFullscreenNavigatorParamList = { [SCREENS.SEARCH.ROOT]: { q: SearchQueryString; @@ -2789,11 +2799,6 @@ type SearchFullscreenNavigatorParamList = { name?: string; groupBy?: string; }; - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: { - reportID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; - }; }; type SearchAdvancedFiltersParamList = { @@ -2911,6 +2916,14 @@ type TestToolsModalModalNavigatorParamList = { }; }; +type ExpenseReportNavigatorParamList = { + [SCREENS.EXPENSE_REPORT_RHP]: { + reportID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; +}; + type RootNavigatorParamList = PublicScreensParamList & AuthScreensParamList & SearchFullscreenNavigatorParamList; type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; @@ -3019,6 +3032,8 @@ export type { MergeTransactionNavigatorParamList, AttachmentModalScreensParamList, ReportCardActivateNavigatorParamList, + SearchMoneyRequestReportParamList, + ExpenseReportNavigatorParamList, WorkspacesDomainModalNavigatorParamList, DomainSplitNavigatorParamList, DomainScreenName, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 416ac9d0430ee..53b98fdf444c4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6209,24 +6209,28 @@ function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP const rootState = navigationRef.current?.getRootState(); const lastFullScreenRoute = rootState?.routes.findLast((route) => isFullScreenName(route.name)); if (lastFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { - const searchFullScreenRoutes = rootState?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); - const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1); - if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - const lastRouteParams = lastRoute?.params; - const newBackRoute = lastRouteParams && 'backTo' in lastRouteParams ? lastRouteParams?.backTo : undefined; - if (isFromRHP) { - Navigation.dismissModal(); - } - Navigation.isNavigationReady().then(() => { - Navigation.goBack(newBackRoute as Route); - }); - return; - } - Navigation.dismissModal(); + // @TODO: Migrate this logic to the new navigation structure + // const searchFullScreenRoutes = rootState?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); + // const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1); + // if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { + // const lastRouteParams = lastRoute?.params; + // const newBackRoute = lastRouteParams && 'backTo' in lastRouteParams ? lastRouteParams?.backTo : undefined; + // if (isFromRHP) { + // Navigation.dismissModal(); + // } + // Navigation.isNavigationReady().then(() => { + // Navigation.goBack(newBackRoute as Route); + // }); + // return; + // } + // Navigation.dismissModal(); + + // When deleting from search, go back to Super Wide RHP to maintain search context + Navigation.dismissToSuperWideRHP(); return; } if (isFromRHP) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); } Navigation.isNavigationReady().then(() => { Navigation.goBack(backRoute); diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index eaee8f4c9ca9b..6f8884e2b6b80 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -50,6 +50,7 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {getMicroSecondOnyxErrorObject, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {readFileAsync} from '@libs/fileDownload/FileUtils'; import type {MinimalTransaction} from '@libs/Formula'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; import { calculateAmount as calculateIOUAmount, @@ -64,6 +65,7 @@ import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import {validateAmount} from '@libs/MoneyRequestUtils'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; +import isReportOpenInSuperWideRHP from '@libs/Navigation/helpers/isReportOpenInSuperWideRHP'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import {isOffline} from '@libs/Network/NetworkStore'; @@ -989,6 +991,11 @@ function dismissModalAndOpenReportInInboxTab(reportID?: string) { const rhpKey = rootState.routes.at(-1)?.state?.key; if (rhpKey) { const hasMultipleTransactions = Object.values(allTransactions).filter((transaction) => transaction?.reportID === reportID).length > 0; + // When a report is opened in the super wide RHP, we need to dismiss to the first RHP to show the same report with new expense. + if (isReportOpenInSuperWideRHP(rootState)) { + Navigation.dismissToPreviousRHP(); + return; + } // When a report with one expense is opened in the wide RHP and the user adds another expense, RHP should be dismissed and ROUTES.SEARCH_MONEY_REQUEST_REPORT should be displayed. if (hasMultipleTransactions && reportID) { Navigation.dismissModal(); @@ -8630,7 +8637,12 @@ function getNavigationUrlOnMoneyRequestDelete( // Determine which report to navigate back to if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { - return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); + // On narrow layouts (mobile), navigate to the full report screen + if (getIsNarrowLayout()) { + return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); + } + // On wide layouts, navigate to the Super Wide RHP version of the expense report + return ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReport.reportID}); } if (iouReport?.chatReportID && shouldDeleteIOUReport) { @@ -13885,7 +13897,7 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac } if (isSearchPageTopmostFullScreenRoute || !transactionReport?.parentReportID) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); // After the modal is dismissed, remove the transaction thread report screen // to avoid navigating back to a report removed by the split transaction. diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx index dedcb86320794..61cff123d3e03 100644 --- a/src/pages/ReportAddApproverPage.tsx +++ b/src/pages/ReportAddApproverPage.tsx @@ -100,7 +100,7 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA isASAPSubmitBetaEnabled, reportNextStep, ); - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); }, [allApprovers, selectedApproverEmail, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep]); const button = useMemo(() => { diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index d9ebe90b0208e..7b6bd438ac7c2 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -73,8 +73,8 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC return; } assignReportToMe(report, currentUserDetails.accountID, currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep); - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - }, [selectedApproverType, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep, reportID]); + Navigation.dismissToPreviousRHP(); + }, [selectedApproverType, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep]); const approverTypes = useMemo(() => { const data: Array> = [ diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index cfeb0082a01d2..13f16be52a6b7 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -8,6 +8,8 @@ import DragAndDropProvider from '@components/DragAndDrop/Provider'; import MoneyRequestReportView from '@components/MoneyRequestReportView/MoneyRequestReportView'; import ScreenWrapper from '@components/ScreenWrapper'; import {useSearchContext} from '@components/Search/SearchContext'; +import useShowSuperWideRHPVersion from '@components/WideRHPContextProvider/useShowSuperWideRHPVersion'; +import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -19,7 +21,7 @@ import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViol import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {ExpenseReportNavigatorParamList, SearchMoneyRequestReportParamList} from '@libs/Navigation/types'; import { getFilteredReportActionsForReportView, getIOUActionForTransactionID, @@ -40,7 +42,9 @@ import type SCREENS from '@src/SCREENS'; import type {Policy, Transaction, TransactionViolations} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; -type SearchMoneyRequestPageProps = PlatformStackScreenProps; +type SearchMoneyRequestPageProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; const defaultReportMetadata = { isLoadingInitialReportActions: true, @@ -124,6 +128,11 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return {snapshotTransaction: transaction, snapshotViolations: violations}; }, [snapshot?.data, allReportTransactions]); + // If there is more than one transaction, display the report in Super Wide RHP, otherwise it will be shown in Wide RHP + const shouldShowSuperWideRHP = visibleTransactions.length > 1; + + useShowSuperWideRHPVersion(shouldShowSuperWideRHP); + useEffect(() => { if (transactionThreadReportID === CONST.FAKE_REPORT_ID && oneTransactionID) { const iouAction = getIOUActionForTransactionID(reportActions, oneTransactionID); @@ -212,35 +221,37 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { if (shouldUseNarrowLayout) { return ( - - - - + + + - - - - - - - + + + + + + + + + ); } diff --git a/src/pages/TransactionMerge/ConfirmationPage.tsx b/src/pages/TransactionMerge/ConfirmationPage.tsx index 72a673fb5fb3d..4d3e852a907d5 100644 --- a/src/pages/TransactionMerge/ConfirmationPage.tsx +++ b/src/pages/TransactionMerge/ConfirmationPage.tsx @@ -103,7 +103,7 @@ function ConfirmationPage({route}: ConfirmationPageProps) { Navigation.dismissModalWithReport({reportID: reportIDToDismiss}); } } else { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); } }; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index b1c36fe24f97b..bbecb865ee2ee 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -163,12 +163,12 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const splitExpenseWithoutID = {...splitExpenses.at(0), transactionID: ''}; // When we try to save one split during splits creation and if the data is identical to the original transaction we should close the split flow if (!childTransactions.length && deepEqual(splitFieldDataFromOriginalTransactionWithoutID, splitExpenseWithoutID)) { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); return; } // When we try to save splits during editing splits and if the data is identical to the already created transactions we should close the split flow if (childTransactions.length && deepEqual(splitFieldDataFromChildTransactions, splitExpenses)) { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); return; } // When we try to save one split during splits creation and if the data is not identical to the original transaction we should show the error @@ -196,7 +196,7 @@ function SplitExpensePage({route}: SplitExpensePageProps) { // When we try to save splits during editing splits and if the data is identical to the already created transactions we should close the split flow if (deepEqual(splitFieldDataFromChildTransactions, splitExpenses)) { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); return; } diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index b9625fd07474c..1c413dfd0aa50 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -68,7 +68,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const handleGoBack = () => { if (isEditing) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); } else { Navigation.goBack(backTo); } @@ -167,7 +167,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { if (!transaction) { return; } - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { changeTransactionsReport({ diff --git a/src/styles/index.ts b/src/styles/index.ts index 1f56011feb3cd..7506658f08659 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -7,8 +7,8 @@ import type {LineLayer} from 'react-map-gl'; import type {Animated, ImageStyle, TextStyle, ViewStyle} from 'react-native'; import {Platform, StyleSheet} from 'react-native'; import type {PickerStyle} from 'react-native-picker-select'; -import {interpolate} from 'react-native-reanimated'; import type {SharedValue} from 'react-native-reanimated'; +import {interpolate} from 'react-native-reanimated'; import type {MixedStyleDeclaration, MixedStyleRecord} from 'react-native-render-html'; import type {ValueOf} from 'type-fest'; import type DotLottieAnimation from '@components/LottieAnimations/types'; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 9362e696b1168..a934102a8c05e 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -110,6 +110,7 @@ export default { chooseFilesViewMargin: 8, sideBarWithLHBWidth: 320, navigationTabBarSize: 72, + popoverMargin: 18, pdfPageMaxWidth: 992, tooltipZIndex: 10050, gutterWidth: 12, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index fa9cfcf39136c..b33dbfc5d96fb 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -110,6 +110,8 @@ const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), + dismissToPreviousRHP: jest.fn(), + dismissToSuperWideRHP: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), getTopmostReportId: jest.fn(() => topMostReportID), diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 92c7b77456e87..4496f145ef57c 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -48,6 +48,7 @@ jest.mock('@react-navigation/native', () => { useIsFocused: () => true, useNavigationState: () => {}, useFocusEffect: jest.fn(), + useRoute: () => jest.fn(), }; }); diff --git a/tests/utils/TestNavigationContainer.tsx b/tests/utils/TestNavigationContainer.tsx index 8ae0725f83bf2..007b8791dbd50 100644 --- a/tests/utils/TestNavigationContainer.tsx +++ b/tests/utils/TestNavigationContainer.tsx @@ -120,10 +120,6 @@ function TestSearchFullscreenNavigator() { name={SCREENS.SEARCH.ROOT} getComponent={getEmptyComponent()} /> - ); } From 594b8d6a4144441dd28b51e463661cb0b036df4d Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Mon, 15 Dec 2025 18:01:00 +0100 Subject: [PATCH 04/38] fix swrhp bugs --- .../MoneyRequestReportGroupHeader.tsx | 4 ++-- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- src/libs/SettlementButtonUtils.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx index cd9ba8646637c..5d561637bcfe4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx @@ -4,7 +4,7 @@ import Checkbox from '@components/Checkbox'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; @@ -64,7 +64,7 @@ function MoneyRequestReportGroupHeader({ }: MoneyRequestReportGroupHeaderProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const cleanedGroupName = isGroupedByTag && group.groupName ? getCommaSeparatedTagNameWithSanitizedColons(group.groupName) : group.groupName; const displayName = cleanedGroupName || translate(isGroupedByTag ? 'reportLayout.noTag' : 'reportLayout.uncategorized'); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 8f547047cb0e8..52a256c2c8d42 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -128,7 +128,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla } const focusedRoute = findFocusedRoute(state); - if (focusedRoute?.name === SCREENS.REPORT) { + if (focusedRoute?.name === SCREENS.REPORT || focusedRoute?.name === SCREENS.EXPENSE_REPORT_RHP) { // We're guaranteed that the type of params is of SCREENS.REPORT return (focusedRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]).reportID; } diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 246f9d985676c..d181d9c0a8045 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -43,6 +43,10 @@ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID: chatReportID})), navigate: () => Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: chatReportID})), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, ]; if (reportID === undefined) { @@ -62,6 +66,10 @@ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, ]; return [...nonReportIdRouteMappings, ...reportIdRouteMappings]; From 8fc291129fa134d6856afeb7d2393b761b219495 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 12 Dec 2025 14:18:46 +0100 Subject: [PATCH 05/38] Flatten super wide rhp structure --- src/SCREENS.ts | 1 - .../getVisibleRHPRouteKeys.ts | 6 +----- .../ModalStackNavigators/index.tsx | 13 ------------ .../Navigators/RightModalNavigator.tsx | 13 +++++++----- src/libs/Navigation/linkingConfig/config.ts | 12 ++--------- src/libs/Navigation/types.ts | 21 +++++++++---------- .../Search/SearchMoneyRequestReportPage.tsx | 6 +++--- 7 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b43b245fafe55..e7bf33e605e8c 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -278,7 +278,6 @@ const SCREENS = { EXPENSE_REPORT: 'ExpenseReport', }, REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root', - EXPENSE_REPORT_RHP: 'Expense_Report_RHP', PUBLIC_CONSOLE_DEBUG: 'Console_Debug', SAML_SIGN_IN: 'SAMLSignIn', WORKSPACE_JOIN_USER: 'WorkspaceJoinUser', diff --git a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts index 416c5baa02298..acb30d1ac1d7b 100644 --- a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts +++ b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts @@ -34,11 +34,7 @@ function getVisibleRHPKeys(allSuperWideRHPKeys: string[], allWideRHPKeys: string return emptyRHPKeysState; } - const superWideRHPIndex = - lastRHPRoute.state?.routes.findLastIndex((route) => { - const focusedRouteKey = route.state?.routes?.at(0)?.key; - return focusedRouteKey && allSuperWideRHPKeys.includes(focusedRouteKey); - }) ?? -1; + const superWideRHPIndex = lastRHPRoute.state?.routes.findLastIndex((route) => route?.key && allSuperWideRHPKeys.includes(route.key)) ?? -1; const wideRHPIndex = lastRHPRoute.state?.routes.findLastIndex((route) => route?.key && allWideRHPKeys.includes(route.key)) ?? -1; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0c7e87b0657fa..8e70d36864994 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -13,7 +13,6 @@ import type { DebugParamList, EditRequestNavigatorParamList, EnablePaymentsNavigatorParamList, - ExpenseReportNavigatorParamList, FlagCommentNavigatorParamList, MergeTransactionNavigatorParamList, MissingPersonalDetailsParamList, @@ -37,7 +36,6 @@ import type { ScheduleCallParamList, SearchAdvancedFiltersParamList, SearchColumnsParamList, - SearchMoneyRequestReportParamList, SearchReportActionsParamList, SearchSavedSearchParamList, SettingsNavigatorParamList, @@ -885,11 +883,6 @@ const SearchReportActionsModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchTransactionsChangeReport').default, }); -// This navigator is reserved for the screen that can be displayed as Super Wide RHP, other screens should not be added here. -const SearchMoneyRequestReportModalStackNavigator = createModalStackNavigator({ - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: () => require('../../../../pages/Search/SearchMoneyRequestReportPage').default, -}); - const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator({ [SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTypePage').default, @@ -977,10 +970,6 @@ const ScheduleCallModalStackNavigator = createModalStackNavigator require('../../../../pages/ScheduleCall/ScheduleCallConfirmationPage').default, }); -const ExpenseReportModalStackNavigator = createModalStackNavigator({ - [SCREENS.EXPENSE_REPORT_RHP]: () => require('../../../../pages/Search/SearchMoneyRequestReportPage').default, -}); - const WorkspacesDomainModalStackNavigator = createModalStackNavigator({ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/WorkspacesVerifyDomainPage').default, [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/WorkspacesDomainVerifiedPage').default, @@ -1039,6 +1028,4 @@ export { WorkspaceConfirmationModalStackNavigator, WorkspaceDuplicateModalStackNavigator, WorkspacesDomainModalStackNavigator, - SearchMoneyRequestReportModalStackNavigator, - ExpenseReportModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 1e4f5f9c9c1ba..6124b0c388327 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -84,6 +84,7 @@ function SecondaryOverlay() { } const loadRHPReportScreen = () => require('../../../../pages/home/RHPReportScreen').default; +const loadSearchMoneyRequestReportPage = () => require('../../../../pages/Search/SearchMoneyRequestReportPage').default; function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -351,10 +352,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { return {...options, animation: animationEnabledOnSearchReport ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; }} /> - + ['config'] = { [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP, }, }, - [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: { - screens: { - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT.route, - }, - }, [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: { screens: { [SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(), @@ -1875,11 +1870,8 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, }, }, - [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: { - screens: { - [SCREENS.EXPENSE_REPORT_RHP]: ROUTES.EXPENSE_REPORT_RHP.route, - }, - }, + [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: ROUTES.EXPENSE_REPORT_RHP.route, + [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT.route, [SCREENS.RIGHT_MODAL.DOMAIN]: { screens: { [SCREENS.WORKSPACES_VERIFY_DOMAIN]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ce69cedf1b3a8..7256d71a635bb 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2241,7 +2241,6 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TRAVEL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: NavigatorScreenParams; @@ -2253,7 +2252,16 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: { + reportID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; + [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: { + reportID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { @@ -2916,14 +2924,6 @@ type TestToolsModalModalNavigatorParamList = { }; }; -type ExpenseReportNavigatorParamList = { - [SCREENS.EXPENSE_REPORT_RHP]: { - reportID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; - }; -}; - type RootNavigatorParamList = PublicScreensParamList & AuthScreensParamList & SearchFullscreenNavigatorParamList; type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; @@ -3033,7 +3033,6 @@ export type { AttachmentModalScreensParamList, ReportCardActivateNavigatorParamList, SearchMoneyRequestReportParamList, - ExpenseReportNavigatorParamList, WorkspacesDomainModalNavigatorParamList, DomainSplitNavigatorParamList, DomainScreenName, diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 13f16be52a6b7..37cd1255ce172 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -21,7 +21,7 @@ import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViol import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ExpenseReportNavigatorParamList, SearchMoneyRequestReportParamList} from '@libs/Navigation/types'; +import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; import { getFilteredReportActionsForReportView, getIOUActionForTransactionID, @@ -43,8 +43,8 @@ import type {Policy, Transaction, TransactionViolations} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; type SearchMoneyRequestPageProps = - | PlatformStackScreenProps - | PlatformStackScreenProps; + | PlatformStackScreenProps + | PlatformStackScreenProps; const defaultReportMetadata = { isLoadingInitialReportActions: true, From 13ca711952fb2e713b862a83cfe0e39e1acb4dc4 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 15 Dec 2025 19:45:09 +0100 Subject: [PATCH 06/38] Add SuperWideRHPWrapper --- .../RHPWrapper/SecondaryOverlay.tsx | 56 +++++++++++++++++++ .../SuperWideRHPWrapper/index.native.tsx | 7 +++ .../RHPWrapper/SuperWideRHPWrapper/index.tsx | 17 ++++++ src/components/RHPWrapper/TertiaryOverlay.tsx | 22 ++++++++ .../Search/SearchMoneyRequestReportPage.tsx | 1 - 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/components/RHPWrapper/SecondaryOverlay.tsx create mode 100644 src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx create mode 100644 src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx create mode 100644 src/components/RHPWrapper/TertiaryOverlay.tsx diff --git a/src/components/RHPWrapper/SecondaryOverlay.tsx b/src/components/RHPWrapper/SecondaryOverlay.tsx new file mode 100644 index 0000000000000..7b25a0e474b29 --- /dev/null +++ b/src/components/RHPWrapper/SecondaryOverlay.tsx @@ -0,0 +1,56 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useContext} from 'react'; +import { + animatedReceiptPaneRHPWidth, + modalStackOverlaySuperWideRHPPositionLeft, + modalStackOverlayWideRHPPositionLeft, + secondOverlayRHPOnSuperWideRHPProgress, + secondOverlayRHPOnWideRHPProgress, + secondOverlayWideRHPProgress, + WideRHPContext, +} from '@components/WideRHPContextProvider'; +import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; + +export default function SecondaryOverlay() { + const {shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys, wideRHPRouteKeys} = + useContext(WideRHPContext); + + const route = useRoute(); + + const isWide = !!route?.key && wideRHPRouteKeys.includes(route.key); + const isSuperWide = !!route?.key && superWideRHPRouteKeys.includes(route.key); + + const isRHPDisplayedOnWideRHP = shouldRenderSecondaryOverlayForRHPOnWideRHP && isWide; + const isRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForRHPOnSuperWideRHP && isSuperWide; + const isWideRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForWideRHP && isSuperWide; + + if (isRHPDisplayedOnWideRHP) { + return ( + + ); + } + + if (isWideRHPDisplayedOnSuperWideRHP) { + return ( + + ); + } + + if (isRHPDisplayedOnSuperWideRHP) { + return ( + + ); + } + + return null; +} diff --git a/src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx b/src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx new file mode 100644 index 0000000000000..a7f2d680e56e2 --- /dev/null +++ b/src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx @@ -0,0 +1,7 @@ +type WideRHPWrapperProps = { + children: React.ReactNode; +}; + +export default function SuperWideRHPWrapper({children}: WideRHPWrapperProps) { + return children; +} diff --git a/src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx b/src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx new file mode 100644 index 0000000000000..3d82fa24b24b3 --- /dev/null +++ b/src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import SecondaryOverlay from '@components/RHPWrapper/SecondaryOverlay'; +import TertiaryOverlay from '@components/RHPWrapper/TertiaryOverlay'; + +type WideRHPWrapperProps = { + children: React.ReactNode; +}; + +export default function SuperWideRHPWrapper({children}: WideRHPWrapperProps) { + return ( + <> + {children} + + + + ); +} diff --git a/src/components/RHPWrapper/TertiaryOverlay.tsx b/src/components/RHPWrapper/TertiaryOverlay.tsx new file mode 100644 index 0000000000000..b746e414d9e2f --- /dev/null +++ b/src/components/RHPWrapper/TertiaryOverlay.tsx @@ -0,0 +1,22 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useContext} from 'react'; +import {modalStackOverlaySuperWideRHPPositionLeft, thirdOverlayProgress, WideRHPContext} from '@components/WideRHPContextProvider'; +import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; + +export default function TertiaryOverlay() { + const {shouldRenderTertiaryOverlay, wideRHPRouteKeys} = useContext(WideRHPContext); + const route = useRoute(); + + const isWide = route?.key && wideRHPRouteKeys.includes(route.key); + + if (isWide && shouldRenderTertiaryOverlay) { + return ( + + ); + } + + return null; +} diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 37cd1255ce172..5f485ea5c3154 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -59,7 +59,6 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID); const {currentSearchHash} = useSearchContext(); const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); From 1e8c5a0ef3b6342042cbaf1096d2fe0cb5e30990 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 16 Dec 2025 13:35:03 +0100 Subject: [PATCH 07/38] Flatten search report modal stack --- src/components/MoneyReportHeader.tsx | 9 ++-- src/components/Navigation/SearchSidebar.tsx | 3 +- src/components/ParentNavigationSubtitle.tsx | 8 +-- .../RHPWrapper/SecondaryOverlay.tsx | 12 +++++ .../SuperWideRHPWrapper/index.native.tsx | 4 +- .../RHPWrapper/SuperWideRHPWrapper/index.tsx | 30 +++++++++-- src/components/RHPWrapper/TertiaryOverlay.tsx | 2 + .../RHPWrapper/WideRHPWrapper/index.native.ts | 7 +++ .../RHPWrapper/WideRHPWrapper/index.tsx | 50 +++++++++++++++++++ .../ScrollOffsetContextProvider.tsx | 4 +- .../playbackContextReportIDUtils.ts | 8 ++- .../getVisibleRHPRouteKeys.ts | 1 - .../useModalStackScreenOptions.ts | 1 + src/libs/Navigation/Navigation.ts | 9 ++-- .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 3 +- src/libs/Navigation/types.ts | 15 +++--- src/libs/ReportActionComposeFocusManager.ts | 3 +- 17 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 src/components/RHPWrapper/WideRHPWrapper/index.native.ts create mode 100644 src/components/RHPWrapper/WideRHPWrapper/index.tsx diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 5725a4794dab3..5e3c584bbafd7 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -46,7 +46,7 @@ import Log from '@libs/Log'; import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList, SearchMoneyRequestReportParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import { buildOptimisticNextStepForDEWOfflineSubmission, buildOptimisticNextStepForDynamicExternalWorkflowError, @@ -197,7 +197,8 @@ function MoneyReportHeader({ const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth; const route = useRoute< | PlatformStackRouteProp - | PlatformStackRouteProp + | PlatformStackRouteProp + | PlatformStackRouteProp | PlatformStackRouteProp >(); const {login: currentUserLogin, accountID, email} = useCurrentUserPersonalDetails(); @@ -511,9 +512,9 @@ function MoneyReportHeader({ const shouldShowLoadingBar = useLoadingBarVisibility(); const kycWallRef = useContext(KYCWallContext); - const isReportInRHP = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; + const isReportInRHP = route.name !== SCREENS.REPORT; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; - const isReportInSearch = route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT; + const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT; const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport); const isChatReportDM = isDM(chatReport); diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index dffea363935b2..3868b387fa2fc 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -55,7 +55,8 @@ function SearchSidebar({state}: SearchSidebarProps) { setLastSearchType(currentSearchResults.type); }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.type]); - const shouldShowLoadingState = route?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading; + // @TODO: Check this case + const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading; if (shouldUseNarrowLayout) { return null; diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 8a3fb2d3a37d4..4a38ffe651d18 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -12,7 +12,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; -import type {SearchMoneyRequestReportParamList} from '@libs/Navigation/types'; import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -123,7 +122,8 @@ function ParentNavigationSubtitle({ if (currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR && isMoneyRequestReport(report)) { const lastRoute = currentFullScreenRoute?.state?.routes.at(-1); if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - const moneyRequestReportID = (lastRoute?.params as SearchMoneyRequestReportParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID; + // @TODO: Fix this case + const moneyRequestReportID = lastRoute?.params?.reportID; // If the parent report is already displayed underneath RHP, simply dismiss the modal if (moneyRequestReportID === parentReportID) { Navigation.dismissModal(); @@ -139,7 +139,8 @@ function ParentNavigationSubtitle({ if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) { const lastPreviousRoute = previousRoute.state?.routes.at(-1); - const moneyRequestReportID = (lastPreviousRoute?.params as SearchMoneyRequestReportParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID; + // @TODO: Fix this case + const moneyRequestReportID = lastPreviousRoute?.params?.reportID; if (moneyRequestReportID === parentReportID && lastPreviousRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { Navigation.goBack(); @@ -173,6 +174,7 @@ function ParentNavigationSubtitle({ const previousRoute = currentFocusedNavigator?.state?.routes.at(-2); if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + // @TODO: Check this case if (previousRoute.params && 'reportID' in previousRoute.params) { const reportIDFromParams = previousRoute.params.reportID; diff --git a/src/components/RHPWrapper/SecondaryOverlay.tsx b/src/components/RHPWrapper/SecondaryOverlay.tsx index 7b25a0e474b29..f971db206e199 100644 --- a/src/components/RHPWrapper/SecondaryOverlay.tsx +++ b/src/components/RHPWrapper/SecondaryOverlay.tsx @@ -24,6 +24,18 @@ export default function SecondaryOverlay() { const isRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForRHPOnSuperWideRHP && isSuperWide; const isWideRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForWideRHP && isSuperWide; + /** + * These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen + * Their position is calculated as follows: + * The width of the window for which we calculate the overlay positions is the width of the RHP window, for example for Super Wide RHP it will be 1260 px on a wide layout. + * We need to move the overlay left from the left edge of the RHP below to the left edge of the RHP above. + * To calculate this, subtract the width of the widest RHP from the width of the RHP above. + * Please note that in these cases, the overlay is rendered from the RHP screen displayed below. For example, if we display RHP on Wide RHP, the secondary overlay is rendered from Wide RHP, etc. + * Three cases were described for the secondary overlay: + * 1. Single RHP is displayed on Wide RHP + * 2. Single RHP is displayed on Super Wide RHP + * 3. Wide RHP is displayed on Super Wide RHP route. + * */ if (isRHPDisplayedOnWideRHP) { return ( () => { + // Synchronization after RHP unmount is handled in RightModalNavigator.tsx. + const isRHPOpened = navigationRef?.getRootState()?.routes?.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; + if (!isRHPOpened) { + return; + } + + syncRHPKeys(); + }, + [syncRHPKeys], + ), + ); + return ( <> {children} + {/* These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen */} diff --git a/src/components/RHPWrapper/TertiaryOverlay.tsx b/src/components/RHPWrapper/TertiaryOverlay.tsx index b746e414d9e2f..7cfae8ff62d16 100644 --- a/src/components/RHPWrapper/TertiaryOverlay.tsx +++ b/src/components/RHPWrapper/TertiaryOverlay.tsx @@ -9,6 +9,8 @@ export default function TertiaryOverlay() { const isWide = route?.key && wideRHPRouteKeys.includes(route.key); + // This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen + // There is a special case where three different RHP widths are displayed at the same time. In this case, an overlay under RHP should be rendered from Wide RHP. if (isWide && shouldRenderTertiaryOverlay) { return ( () => { + if (!shouldShow) { + return; + } + + // Synchronization after RHP unmount is handled in RightModalNavigator.tsx. + const isRHPOpened = navigationRef?.getRootState()?.routes?.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; + if (!isRHPOpened) { + return; + } + + syncRHPKeys(); + }, + [shouldShow, syncRHPKeys], + ), + ); + + if (!shouldShow) { + return children; + } + + return ( + <> + {children} + {/* These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen */} + + + + ); +} diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index 790287845b26c..f77009bdd9c02 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -108,7 +108,9 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp const routeName = focusedRoute?.name; const isSearchScreen = routeName === SCREENS.SEARCH.ROOT; - const isSearchMoneyRequestReport = routeName === SCREENS.SEARCH.MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_REPORT; + // @TODO: Check this case + const isSearchMoneyRequestReport = + routeName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_REPORT; const scrollOffsetKeys = Object.keys(scrollOffsetsRef.current); diff --git a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts index aec9590bd6408..f406e03c62180 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts +++ b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts @@ -47,7 +47,13 @@ const getCurrentRouteReportID: (url: string) => string | ProtectedCurrentRouteRe return isFocusedRouteAChatThread ? firstReportThatHasURLInAttachments : focusedRouteReportID; }; -const screensWithReportID = [SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.REPORT, SCREENS.SEARCH.MONEY_REQUEST_REPORT, SCREENS.REPORT_ATTACHMENTS]; +const screensWithReportID = [ + SCREENS.RIGHT_MODAL.SEARCH_REPORT, + SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, + SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + SCREENS.REPORT, + SCREENS.REPORT_ATTACHMENTS, +]; function hasReportIdInRouteParams(route: SearchRoute): route is RouteWithReportIDInParams { return !!route && !!route.params && !!screensWithReportID.find((screen) => screen === route.name) && 'reportID' in route.params; diff --git a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts index acb30d1ac1d7b..4d1ff9b4c210f 100644 --- a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts +++ b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts @@ -35,7 +35,6 @@ function getVisibleRHPKeys(allSuperWideRHPKeys: string[], allWideRHPKeys: string } const superWideRHPIndex = lastRHPRoute.state?.routes.findLastIndex((route) => route?.key && allSuperWideRHPKeys.includes(route.key)) ?? -1; - const wideRHPIndex = lastRHPRoute.state?.routes.findLastIndex((route) => route?.key && allWideRHPKeys.includes(route.key)) ?? -1; let visibleRHPKeys; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index 7c6a1f4827bed..58c9ce76d3aab 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -1,6 +1,7 @@ import type {ParamListBase} from '@react-navigation/native'; import {CardStyleInterpolators} from '@react-navigation/stack'; import {useCallback, useContext} from 'react'; +import {Platform} from 'react-native'; import {WideRHPContext} from '@components/WideRHPContextProvider'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 71f0ad20cbae8..7f137f0d6f62a 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -38,14 +38,13 @@ import {linkingConfig} from './linkingConfig'; import {SPLIT_TO_SIDEBAR} from './linkingConfig/RELATIONS'; import navigationRef from './navigationRef'; import type { - ExpenseReportNavigatorParamList, NavigationPartialRoute, NavigationRef, NavigationRoute, NavigationStateRoute, ReportsSplitNavigatorParamList, + RightModalNavigatorParamList, RootNavigatorParamList, - SearchMoneyRequestReportParamList, State, } from './types'; @@ -630,11 +629,11 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g } const topmostSuperWideRHP = topmostSuperWideRHPModalStack.state?.routes.findLast( - (route) => route.name === SCREENS.EXPENSE_REPORT_RHP || route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT, + (route) => route.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, ); const topmostReportParams = topmostSuperWideRHP?.params as - | SearchMoneyRequestReportParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT] - | ExpenseReportNavigatorParamList[typeof SCREENS.EXPENSE_REPORT_RHP] + | RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT] + | RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.EXPENSE_REPORT] | undefined; return topmostReportParams?.reportID; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index 66cdc5794eedc..de08480ebd507 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -50,8 +50,9 @@ const SEARCH_TO_RHP: Partial; [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { @@ -2792,14 +2798,6 @@ type SearchReportActionsParamList = { }; }; -type SearchMoneyRequestReportParamList = { - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: { - reportID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; - }; -}; - type SearchFullscreenNavigatorParamList = { [SCREENS.SEARCH.ROOT]: { q: SearchQueryString; @@ -3032,7 +3030,6 @@ export type { MergeTransactionNavigatorParamList, AttachmentModalScreensParamList, ReportCardActivateNavigatorParamList, - SearchMoneyRequestReportParamList, WorkspacesDomainModalNavigatorParamList, DomainSplitNavigatorParamList, DomainScreenName, diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 6f9194ac60651..aee74a59cf149 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -42,7 +42,8 @@ function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) { /** Do not trigger the refocusing when the active route is not the report screen */ const navigationState = navigationRef.getState(); const focusedRoute = findFocusedRoute(navigationState); - if (!navigationState || (!isReportOpenInRHP(navigationState) && focusedRoute?.name !== SCREENS.REPORT && focusedRoute?.name !== SCREENS.SEARCH.MONEY_REQUEST_REPORT)) { + // @TODO: Check this case + if (!navigationState || (!isReportOpenInRHP(navigationState) && focusedRoute?.name !== SCREENS.REPORT)) { return; } From 21a70bfe1f1005afa54582e06136199319ab3434 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 16 Dec 2025 15:17:29 +0100 Subject: [PATCH 08/38] small fix --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 52a256c2c8d42..2603073e3fc6b 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -128,7 +128,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla } const focusedRoute = findFocusedRoute(state); - if (focusedRoute?.name === SCREENS.REPORT || focusedRoute?.name === SCREENS.EXPENSE_REPORT_RHP) { + if (focusedRoute?.name === SCREENS.REPORT || focusedRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { // We're guaranteed that the type of params is of SCREENS.REPORT return (focusedRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]).reportID; } From 0f91145ef31d91bb0f270893758f6b81395dd80d Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 16 Dec 2025 17:25:58 +0100 Subject: [PATCH 09/38] migrate navigation to wide rhp modal --- src/components/AddUnreportedExpenseFooter.tsx | 2 +- src/pages/iou/request/step/IOURequestEditReport.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AddUnreportedExpenseFooter.tsx b/src/components/AddUnreportedExpenseFooter.tsx index 58defd3616309..b7b2355a93a11 100644 --- a/src/components/AddUnreportedExpenseFooter.tsx +++ b/src/components/AddUnreportedExpenseFooter.tsx @@ -51,7 +51,7 @@ function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, repor setErrorMessage(translate('iou.selectUnreportedExpense')); return; } - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { if (report && isIOUReport(report)) { diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 03ee0e7fe4c99..f059f2887ccf0 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -57,7 +57,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { const selectReport = (item: TransactionGroupListItem, report?: OnyxEntry) => { if (selectedTransactionIDs.length === 0 || item.value === reportID) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); return; } @@ -79,7 +79,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { clearSelectedTransactions(true); }); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }; const removeFromReport = () => { @@ -97,7 +97,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { turnOffMobileSelectionMode(); } clearSelectedTransactions(true); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }; const createReportForPolicy = (shouldDismissEmptyReportsConfirmation?: boolean) => { From 51d2f2c0c049d92601bbb6b11dd7dae1b1f24605 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 16 Dec 2025 17:47:20 +0100 Subject: [PATCH 10/38] migrate navigation part to flatten structure of wide rhps --- src/libs/Navigation/Navigation.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 7f137f0d6f62a..416511d6124af 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -615,22 +615,18 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g if (!state) { return; } - const topmostRightModalNavigator = state.routes?.at(-1); if (!topmostRightModalNavigator || topmostRightModalNavigator.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { return; } - const topmostSuperWideRHPModalStack = topmostRightModalNavigator.state?.routes.findLast((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name)); + const topmostSuperWideRHP = topmostRightModalNavigator.state?.routes.findLast((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name)); - if (!topmostSuperWideRHPModalStack) { + if (!topmostSuperWideRHP) { return; } - const topmostSuperWideRHP = topmostSuperWideRHPModalStack.state?.routes.findLast( - (route) => route.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, - ); const topmostReportParams = topmostSuperWideRHP?.params as | RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT] | RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.EXPENSE_REPORT] @@ -674,7 +670,7 @@ const dismissModalWithReport = ({reportID, reportActionID, referrer, backTo}: Re let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { - dismissToPreviousRHP(); + dismissToSuperWideRHP(); return; } From 1a67465a53398eb74e5261c590a316e4b3227f59 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 16 Dec 2025 18:19:34 +0100 Subject: [PATCH 11/38] fix navigation to super wide rhp after merging reports --- src/pages/TransactionMerge/ConfirmationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/TransactionMerge/ConfirmationPage.tsx b/src/pages/TransactionMerge/ConfirmationPage.tsx index 4d3e852a907d5..488edf28ff15d 100644 --- a/src/pages/TransactionMerge/ConfirmationPage.tsx +++ b/src/pages/TransactionMerge/ConfirmationPage.tsx @@ -103,7 +103,7 @@ function ConfirmationPage({route}: ConfirmationPageProps) { Navigation.dismissModalWithReport({reportID: reportIDToDismiss}); } } else { - Navigation.dismissToPreviousRHP(); + Navigation.dismissToSuperWideRHP(); } }; From 624dce955666d7cb56a80e929de2baa7cf21a017 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 17 Dec 2025 11:29:15 +0100 Subject: [PATCH 12/38] fix navigation to super wide RHP after reviewing duplicates --- src/pages/TransactionDuplicate/Confirmation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx index a923c5c75b126..096b79e94c35a 100644 --- a/src/pages/TransactionDuplicate/Confirmation.tsx +++ b/src/pages/TransactionDuplicate/Confirmation.tsx @@ -79,12 +79,12 @@ function Confirmation() { transactionsMergeParams.transactionThreadReportID = transactionThreadReportID; } mergeDuplicates(transactionsMergeParams); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }, [reportAction?.childReportID, transactionsMergeParams]); const handleResolveDuplicates = useCallback(() => { resolveDuplicates(transactionsMergeParams); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }, [transactionsMergeParams]); const contextValue = useMemo( From 5b5247e0718ee6b6ca9e9c7adcd2ffba81e646e2 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 17 Dec 2025 15:02:57 +0100 Subject: [PATCH 13/38] fix leave button and navigation to super wide RHP --- src/libs/Navigation/Navigation.ts | 1 + src/libs/actions/Report.ts | 14 ++++++++++++-- src/pages/ReportDetailsPage.tsx | 2 +- src/pages/Search/SearchMoneyRequestReportPage.tsx | 15 ++++++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 416511d6124af..111d1822024e9 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -884,6 +884,7 @@ export default { dismissToPreviousRHP, dismissToSuperWideRHP, getTopmostReportIDInSearchRHP, + getTopmostSuperWideRHPReportID, }; export {navigationRef}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 39c1d867ae722..1d42c1865ee34 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -72,6 +72,7 @@ import getEnvironment from '@libs/Environment/getEnvironment'; import type EnvironmentType from '@libs/Environment/getEnvironment/types'; import {getMicroSecondOnyxErrorWithTranslationKey, getMicroSecondTranslationErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import HttpUtils from '@libs/HttpUtils'; import Log from '@libs/Log'; import {isEmailPublicDomain} from '@libs/LoginUtils'; @@ -3692,8 +3693,17 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { const lastAccessedReportID = findLastAccessedReport(false, false, undefined, currentReport?.reportID)?.reportID; if (lastAccessedReportID) { - const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); - Navigation.goBack(lastAccessedReportRoute); + // Check if route exists for super wide RHP vs regular full screen report + const routeWithLastAccessedReportID = Navigation.getReportRouteByID(lastAccessedReportID); + const topmostSuperWideRHP = Navigation.getTopmostSuperWideRHPReportID(); + + if (lastAccessedReportID === topmostSuperWideRHP && !getIsNarrowLayout()) { + const lastAccessedReportRoute = ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: lastAccessedReportID}); + Navigation.goBack(lastAccessedReportRoute); + } else { + const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); + Navigation.goBack(lastAccessedReportRoute); + } } else { const isChatThread = isChatThreadReportUtils(currentReport); diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 87dbfad34d2fa..fe715f0c83db7 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -327,7 +327,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered, isSelfDM]); const leaveChat = useCallback(() => { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); Navigation.isNavigationReady().then(() => { if (isRootGroupChat) { leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID, currentUserPersonalDetails.accountID); diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 5f485ea5c3154..7015f24e66c30 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -1,4 +1,5 @@ import {PortalHost} from '@gorhom/portal'; +import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import type {FlatList} from 'react-native'; import {View} from 'react-native'; @@ -34,11 +35,12 @@ import {isValidReportIDFromPath} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import ReactionListWrapper from '@pages/home/ReactionListWrapper'; import {createTransactionThreadReport, openReport} from '@userActions/Report'; +import {updateLastVisitTime} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ActionListContextType, ScrollPosition} from '@src/pages/home/ReportScreenContext'; import {ActionListContext} from '@src/pages/home/ReportScreenContext'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type {Policy, Transaction, TransactionViolations} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; @@ -65,6 +67,17 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true, canBeMissing: true}); + const isFocused = useIsFocused(); + + useEffect(() => { + // Update last visit time when the expense super wide RHP report is focused + if (!reportIDFromRoute || !isFocused || route.name !== SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + return; + } + + updateLastVisitTime(reportIDFromRoute); + }, [reportIDFromRoute, isFocused]); + const snapshotReport = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-deprecated return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`] ?? {}) as typeof report; From 879412726f58de3c4e95d950539da420426c11af Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 18 Dec 2025 11:29:48 +0100 Subject: [PATCH 14/38] fix android bugs --- .../ModalStackNavigators/useModalStackScreenOptions.ts | 1 - src/pages/home/ReportScreen.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index 58c9ce76d3aab..7c6a1f4827bed 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -1,7 +1,6 @@ import type {ParamListBase} from '@react-navigation/native'; import {CardStyleInterpolators} from '@react-navigation/stack'; import {useCallback, useContext} from 'react'; -import {Platform} from 'react-native'; import {WideRHPContext} from '@components/WideRHPContextProvider'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e6a752a84ed46..c7ad26b7ef7e9 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -394,7 +394,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr return; } if (isInNarrowPaneModal) { - Navigation.dismissModal(); + Navigation.goBack(); return; } if (backTo) { From cf034238f3eb32df0955a10eca7c4d0b1b0288d1 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 18 Dec 2025 12:25:40 +0100 Subject: [PATCH 15/38] remove dead code --- .../Search/SearchMoneyRequestReportPage.tsx | 88 ++++++------------- src/pages/home/ReportScreen.tsx | 1 - 2 files changed, 25 insertions(+), 64 deletions(-) diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 7015f24e66c30..1996157ad41ac 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -2,7 +2,6 @@ import {PortalHost} from '@gorhom/portal'; import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import type {FlatList} from 'react-native'; -import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -17,6 +16,7 @@ import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -34,8 +34,7 @@ import { import {isValidReportIDFromPath} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import ReactionListWrapper from '@pages/home/ReactionListWrapper'; -import {createTransactionThreadReport, openReport} from '@userActions/Report'; -import {updateLastVisitTime} from '@userActions/Report'; +import {createTransactionThreadReport, openReport, updateLastVisitTime} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ActionListContextType, ScrollPosition} from '@src/pages/home/ReportScreenContext'; @@ -59,6 +58,7 @@ const defaultReportMetadata = { function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout: shouldUseNarrowLayoutOnWideRHP} = useResponsiveLayoutOnWideRHP(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID); @@ -76,7 +76,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { } updateLastVisitTime(reportIDFromRoute); - }, [reportIDFromRoute, isFocused]); + }, [reportIDFromRoute, isFocused, route.name]); const snapshotReport = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -231,51 +231,15 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { [reportID, reportMetadata?.isLoadingInitialReportActions], ); - if (shouldUseNarrowLayout) { - return ( - - - - - - - - - - - - - - ); - } - return ( - - - - + + + + - - - - + + {!shouldUseNarrowLayoutOnWideRHP && } - - - - + + + + ); } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index c7ad26b7ef7e9..7b20dc08b4de3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -1064,5 +1064,4 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr ); } -// eslint-disable-next-line rulesdir/no-deep-equal-in-memo export default memo(ReportScreen, (prevProps, nextProps) => deepEqual(prevProps.route, nextProps.route)); From 287472f6a6f991a52ee307c9a5f8ca8c97d98a8e Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 18 Dec 2025 15:44:05 +0100 Subject: [PATCH 16/38] Remove check for Portal in SearchMoneyRequestReportPage --- .../ModalStackNavigators/useModalStackScreenOptions.ts | 1 - src/pages/Search/SearchMoneyRequestReportPage.tsx | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index 7c6a1f4827bed..685e72b3fb5a7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import enhanceCardStyleInterpolator from '@libs/Navigation/AppNavigator/enhanceCardStyleInterpolator'; import hideKeyboardOnSwipe from '@libs/Navigation/AppNavigator/hideKeyboardOnSwipe'; import type {PlatformStackNavigationOptions, PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import variables from '@styles/variables'; function useWideModalStackScreenOptions() { const styles = useThemeStyles(); diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 1996157ad41ac..edc5da00af3b1 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -16,7 +16,6 @@ import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -58,7 +57,6 @@ const defaultReportMetadata = { function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {shouldUseNarrowLayout: shouldUseNarrowLayoutOnWideRHP} = useResponsiveLayoutOnWideRHP(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID); @@ -257,7 +255,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { key={report?.reportID} backToRoute={route.params.backTo} /> - {!shouldUseNarrowLayoutOnWideRHP && } + From e161860ec73b8c53b588e15b9027c2840271c09b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 22 Dec 2025 09:11:58 +0100 Subject: [PATCH 17/38] Fix SearchReport animations --- src/pages/home/report/RHPReportScreen.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/pages/home/report/RHPReportScreen.tsx diff --git a/src/pages/home/report/RHPReportScreen.tsx b/src/pages/home/report/RHPReportScreen.tsx new file mode 100644 index 0000000000000..ea4eb24bb03fc --- /dev/null +++ b/src/pages/home/report/RHPReportScreen.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; +import ReportScreen from '@pages/home/ReportScreen'; +import type SCREENS from '@src/SCREENS'; + +type PageProps = PlatformStackScreenProps; + +// When switching search reports using arrows SET_PARAMS action is performed, but it's necessary to remount the whole component after changing the reportID due to the hooks dependencies. +// It's suggested to refactor hooks in ReportScreen to remove this file and perform SET_PARAMS without the report screen remount. +export default function RHPReportScreen({route, navigation}: PageProps) { + return ( + + ); +} From ac3fa376a1efbaec3ccadd16ea75683c27216e35 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 22 Dec 2025 10:48:36 +0100 Subject: [PATCH 18/38] Fix displaying side panel above super wide rhp --- src/components/SidePanel/SidePanelModal/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/SidePanel/SidePanelModal/index.tsx b/src/components/SidePanel/SidePanelModal/index.tsx index 467da113af98e..60fb40444e7ce 100644 --- a/src/components/SidePanel/SidePanelModal/index.tsx +++ b/src/components/SidePanel/SidePanelModal/index.tsx @@ -27,8 +27,9 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi const [isRHPVisible = false] = useOnyx(ONYXKEYS.MODAL, {selector: isRHPVisibleSelector, canBeMissing: true}); const uniqueModalId = ComposerFocusManager.getId(); - const {wideRHPRouteKeys, isWideRHPFocused} = useContext(WideRHPContext); - const isWideRHPVisible = !!wideRHPRouteKeys.length; + const {wideRHPRouteKeys, isWideRHPFocused, superWideRHPRouteKeys, isSuperWideRHPFocused} = useContext(WideRHPContext); + + const shouldOverlayBeVisible = (!!wideRHPRouteKeys.length && isWideRHPFocused) || (!!superWideRHPRouteKeys.length && isSuperWideRHPFocused) || !isRHPVisible; const onCloseSidePanelOnSmallScreens = () => { if (isExtraLargeScreenWidth) { @@ -65,7 +66,7 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi {!shouldHideSidePanelBackdrop && ( )} From 31b6b393e46fd7a967f6a31c96208bc2464298c2 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 22 Dec 2025 10:49:34 +0100 Subject: [PATCH 19/38] Fix navigating using ParentNavigationSubtitle --- src/components/ParentNavigationSubtitle.tsx | 24 ++++++--------------- src/libs/actions/Report.ts | 1 - src/pages/home/ReportScreen.tsx | 1 + 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 4a38ffe651d18..172754d47e819 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -12,6 +12,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; +import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -117,32 +118,19 @@ function ParentNavigationSubtitle({ const parentAction = getReportAction(parentReportID, parentReportActionID); const isVisibleAction = shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? CONST.DEFAULT_NUMBER_ID, canUserPerformWriteAction); + const focusedNavigatorState = currentFocusedNavigator?.state; + const currentReportIndex = focusedNavigatorState?.index; + if (openParentReportInCurrentTab && isReportInRHP) { // If the report is displayed in RHP in Reports tab, we want to stay in the current tab after opening the parent report if (currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR && isMoneyRequestReport(report)) { - const lastRoute = currentFullScreenRoute?.state?.routes.at(-1); - if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - // @TODO: Fix this case - const moneyRequestReportID = lastRoute?.params?.reportID; - // If the parent report is already displayed underneath RHP, simply dismiss the modal - if (moneyRequestReportID === parentReportID) { - Navigation.dismissModal(); - return; - } - } - - const focusedNavigatorState = currentFocusedNavigator?.state; - const currentReportIndex = focusedNavigatorState?.index; // Dismiss wide RHP and go back to already opened super wide RHP if the parent report is already opened there if (currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && currentReportIndex && currentReportIndex > 0) { const previousRoute = focusedNavigatorState.routes[currentReportIndex - 1]; if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) { - const lastPreviousRoute = previousRoute.state?.routes.at(-1); - // @TODO: Fix this case - const moneyRequestReportID = lastPreviousRoute?.params?.reportID; - - if (moneyRequestReportID === parentReportID && lastPreviousRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { + const moneyRequestReportID = (previousRoute?.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID; + if (moneyRequestReportID === parentReportID) { Navigation.goBack(); return; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1d42c1865ee34..f5069d302e49d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3694,7 +3694,6 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { if (lastAccessedReportID) { // Check if route exists for super wide RHP vs regular full screen report - const routeWithLastAccessedReportID = Navigation.getReportRouteByID(lastAccessedReportID); const topmostSuperWideRHP = Navigation.getTopmostSuperWideRHPReportID(); if (lastAccessedReportID === topmostSuperWideRHP && !getIsNarrowLayout()) { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 7b20dc08b4de3..c7ad26b7ef7e9 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -1064,4 +1064,5 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr ); } +// eslint-disable-next-line rulesdir/no-deep-equal-in-memo export default memo(ReportScreen, (prevProps, nextProps) => deepEqual(prevProps.route, nextProps.route)); From 37fe97114a7ad909fe4b07042102ac7d37913da9 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 22 Dec 2025 11:31:48 +0100 Subject: [PATCH 20/38] Add ExpenseReportVerifyAccountPage --- src/ROUTES.ts | 4 ++++ src/SCREENS.ts | 1 + src/libs/Navigation/AppNavigator/AuthScreens.tsx | 7 +------ .../AppNavigator/ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 3 +++ src/libs/SettlementButtonUtils.ts | 2 +- .../home/report/ExpenseReportVerifyAccountPage.tsx | 14 ++++++++++++++ 8 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 src/pages/home/report/ExpenseReportVerifyAccountPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e272d8309e223..8bac46cdf4065 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -604,6 +604,10 @@ const ROUTES = { route: `r/:reportID/${VERIFY_ACCOUNT}`, getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}` as const, }, + EXPENSE_REPORT_VERIFY_ACCOUNT: { + route: `e/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `e/${reportID}/${VERIFY_ACCOUNT}` as const, + }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e7bf33e605e8c..7ff78424f6a3f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -825,6 +825,7 @@ const SCREENS = { REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info', REFERRAL_DETAILS: 'Referral_Details', REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', + EXPENSE_REPORT_VERIFY_ACCOUNT: 'Expense_Report_Verify_Account', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', SHARE: { ROOT: 'Share_Root', diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 2df277ec6da37..df3c85bebbd4e 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -348,12 +348,7 @@ function AuthScreens() { return; } - if (shouldRenderSecondaryOverlayForRHPOnWideRHP) { - Navigation.dismissToPreviousRHP(); - return; - } - - if (shouldRenderTertiaryOverlay) { + if (shouldRenderSecondaryOverlayForRHPOnWideRHP || shouldRenderTertiaryOverlay) { Navigation.dismissToPreviousRHP(); return; } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8e70d36864994..b7135912ab8a5 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -259,6 +259,7 @@ const TaskModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ReportVerifyAccountPage').default, + [SCREENS.EXPENSE_REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ExpenseReportVerifyAccountPage').default, [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchReportVerifyAccountPage').default, }); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 39a63b136eb66..e156ce5f1d1f7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1660,6 +1660,7 @@ const config: LinkingOptions['config'] = { [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: { screens: { [SCREENS.REPORT_VERIFY_ACCOUNT]: ROUTES.REPORT_VERIFY_ACCOUNT.route, + [SCREENS.EXPENSE_REPORT_VERIFY_ACCOUNT]: ROUTES.EXPENSE_REPORT_VERIFY_ACCOUNT.route, [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.route, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 48022cb7d852b..1dec1166cca69 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -88,6 +88,9 @@ type ReportVerifyAccountNavigatorParamList = { [SCREENS.REPORT_VERIFY_ACCOUNT]: { reportID: string; }; + [SCREENS.EXPENSE_REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; }; type SettingsNavigatorParamList = { diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index d181d9c0a8045..34d870abb86cc 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -68,7 +68,7 @@ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping }, { check: (activeRoute: string) => activeRoute.includes(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})), - navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + navigate: () => Navigation.navigate(ROUTES.EXPENSE_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), }, ]; diff --git a/src/pages/home/report/ExpenseReportVerifyAccountPage.tsx b/src/pages/home/report/ExpenseReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..19ad75760b6dd --- /dev/null +++ b/src/pages/home/report/ExpenseReportVerifyAccountPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {ReportVerifyAccountNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ExpenseReportVerifyAccountPageProps = PlatformStackScreenProps; + +function ExpenseReportVerifyAccountPage({route}: ExpenseReportVerifyAccountPageProps) { + return ; +} + +export default ExpenseReportVerifyAccountPage; From 193870d28547c8ebb51851dd0cd4a6eddfc767ce Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 22 Dec 2025 14:01:57 +0100 Subject: [PATCH 21/38] Fix issue: Expense - Report RHP opens not here page after resolving duplicates --- src/pages/TransactionDuplicate/Confirmation.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx index 096b79e94c35a..eba4c10cdc0d2 100644 --- a/src/pages/TransactionDuplicate/Confirmation.tsx +++ b/src/pages/TransactionDuplicate/Confirmation.tsx @@ -1,5 +1,5 @@ import {useRoute} from '@react-navigation/native'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useContext, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -12,6 +12,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; +import {WideRHPContext} from '@components/WideRHPContextProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -64,6 +65,8 @@ function Confirmation() { const reportAction = Object.values(reportActions ?? {}).find( (action) => ReportActionsUtils.isMoneyRequestAction(action) && ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID === reviewDuplicates?.transactionID, ); + const {superWideRHPRouteKeys} = useContext(WideRHPContext); + const isSuperWideRHPDisplayed = superWideRHPRouteKeys.length > 0; const [duplicates] = useTransactionsByID(reviewDuplicates?.duplicates); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); @@ -79,8 +82,12 @@ function Confirmation() { transactionsMergeParams.transactionThreadReportID = transactionThreadReportID; } mergeDuplicates(transactionsMergeParams); - Navigation.dismissToSuperWideRHP(); - }, [reportAction?.childReportID, transactionsMergeParams]); + if (isSuperWideRHPDisplayed) { + Navigation.dismissToSuperWideRHP(); + return; + } + Navigation.dismissModal(); + }, [reportAction?.childReportID, transactionsMergeParams, isSuperWideRHPDisplayed]); const handleResolveDuplicates = useCallback(() => { resolveDuplicates(transactionsMergeParams); From f500e07c6ac46853cb310580a3c231781e402d42 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 23 Dec 2025 12:03:46 +0100 Subject: [PATCH 22/38] Remove unused wrapper files --- .../RHPWrapper/SecondaryOverlay.tsx | 68 ------------------- .../SuperWideRHPWrapper/index.native.tsx | 7 -- .../RHPWrapper/SuperWideRHPWrapper/index.tsx | 41 ----------- src/components/RHPWrapper/TertiaryOverlay.tsx | 24 ------- .../RHPWrapper/WideRHPWrapper/index.native.ts | 7 -- .../RHPWrapper/WideRHPWrapper/index.tsx | 50 -------------- 6 files changed, 197 deletions(-) delete mode 100644 src/components/RHPWrapper/SecondaryOverlay.tsx delete mode 100644 src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx delete mode 100644 src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx delete mode 100644 src/components/RHPWrapper/TertiaryOverlay.tsx delete mode 100644 src/components/RHPWrapper/WideRHPWrapper/index.native.ts delete mode 100644 src/components/RHPWrapper/WideRHPWrapper/index.tsx diff --git a/src/components/RHPWrapper/SecondaryOverlay.tsx b/src/components/RHPWrapper/SecondaryOverlay.tsx deleted file mode 100644 index f971db206e199..0000000000000 --- a/src/components/RHPWrapper/SecondaryOverlay.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useContext} from 'react'; -import { - animatedReceiptPaneRHPWidth, - modalStackOverlaySuperWideRHPPositionLeft, - modalStackOverlayWideRHPPositionLeft, - secondOverlayRHPOnSuperWideRHPProgress, - secondOverlayRHPOnWideRHPProgress, - secondOverlayWideRHPProgress, - WideRHPContext, -} from '@components/WideRHPContextProvider'; -import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; - -export default function SecondaryOverlay() { - const {shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys, wideRHPRouteKeys} = - useContext(WideRHPContext); - - const route = useRoute(); - - const isWide = !!route?.key && wideRHPRouteKeys.includes(route.key); - const isSuperWide = !!route?.key && superWideRHPRouteKeys.includes(route.key); - - const isRHPDisplayedOnWideRHP = shouldRenderSecondaryOverlayForRHPOnWideRHP && isWide; - const isRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForRHPOnSuperWideRHP && isSuperWide; - const isWideRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForWideRHP && isSuperWide; - - /** - * These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen - * Their position is calculated as follows: - * The width of the window for which we calculate the overlay positions is the width of the RHP window, for example for Super Wide RHP it will be 1260 px on a wide layout. - * We need to move the overlay left from the left edge of the RHP below to the left edge of the RHP above. - * To calculate this, subtract the width of the widest RHP from the width of the RHP above. - * Please note that in these cases, the overlay is rendered from the RHP screen displayed below. For example, if we display RHP on Wide RHP, the secondary overlay is rendered from Wide RHP, etc. - * Three cases were described for the secondary overlay: - * 1. Single RHP is displayed on Wide RHP - * 2. Single RHP is displayed on Super Wide RHP - * 3. Wide RHP is displayed on Super Wide RHP route. - * */ - if (isRHPDisplayedOnWideRHP) { - return ( - - ); - } - - if (isWideRHPDisplayedOnSuperWideRHP) { - return ( - - ); - } - - if (isRHPDisplayedOnSuperWideRHP) { - return ( - - ); - } - - return null; -} diff --git a/src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx b/src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx deleted file mode 100644 index 9faeb48edee06..0000000000000 --- a/src/components/RHPWrapper/SuperWideRHPWrapper/index.native.tsx +++ /dev/null @@ -1,7 +0,0 @@ -type SuperWideRHPWrapperProps = { - children: React.ReactNode; -}; - -export default function SuperWideRHPWrapper({children}: SuperWideRHPWrapperProps) { - return children; -} diff --git a/src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx b/src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx deleted file mode 100644 index a726ab8f2c264..0000000000000 --- a/src/components/RHPWrapper/SuperWideRHPWrapper/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useContext} from 'react'; -import SecondaryOverlay from '@components/RHPWrapper/SecondaryOverlay'; -import TertiaryOverlay from '@components/RHPWrapper/TertiaryOverlay'; -import {WideRHPContext} from '@components/WideRHPContextProvider'; -import {navigationRef} from '@libs/Navigation/Navigation'; -import NAVIGATORS from '@src/NAVIGATORS'; - -type SuperWideRHPWrapperProps = { - children: React.ReactNode; -}; - -export default function SuperWideRHPWrapper({children}: SuperWideRHPWrapperProps) { - const {syncRHPKeys} = useContext(WideRHPContext); - - // This hook handles the case when a wider RHP is displayed above a narrower one. - // In this situation, we need to synchronize the keys, as superWideRHPKeys and wideRHPKeys store the keys of the screens that are visible. - useFocusEffect( - useCallback( - () => () => { - // Synchronization after RHP unmount is handled in RightModalNavigator.tsx. - const isRHPOpened = navigationRef?.getRootState()?.routes?.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; - if (!isRHPOpened) { - return; - } - - syncRHPKeys(); - }, - [syncRHPKeys], - ), - ); - - return ( - <> - {children} - {/* These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen */} - - - - ); -} diff --git a/src/components/RHPWrapper/TertiaryOverlay.tsx b/src/components/RHPWrapper/TertiaryOverlay.tsx deleted file mode 100644 index 7cfae8ff62d16..0000000000000 --- a/src/components/RHPWrapper/TertiaryOverlay.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useContext} from 'react'; -import {modalStackOverlaySuperWideRHPPositionLeft, thirdOverlayProgress, WideRHPContext} from '@components/WideRHPContextProvider'; -import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; - -export default function TertiaryOverlay() { - const {shouldRenderTertiaryOverlay, wideRHPRouteKeys} = useContext(WideRHPContext); - const route = useRoute(); - - const isWide = route?.key && wideRHPRouteKeys.includes(route.key); - - // This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen - // There is a special case where three different RHP widths are displayed at the same time. In this case, an overlay under RHP should be rendered from Wide RHP. - if (isWide && shouldRenderTertiaryOverlay) { - return ( - - ); - } - - return null; -} diff --git a/src/components/RHPWrapper/WideRHPWrapper/index.native.ts b/src/components/RHPWrapper/WideRHPWrapper/index.native.ts deleted file mode 100644 index 5eadcb4fbc192..0000000000000 --- a/src/components/RHPWrapper/WideRHPWrapper/index.native.ts +++ /dev/null @@ -1,7 +0,0 @@ -type WideRHPWrapperProps = { - children: React.ReactNode; -}; - -export default function WideRHPWrapper({children}: WideRHPWrapperProps) { - return children; -} diff --git a/src/components/RHPWrapper/WideRHPWrapper/index.tsx b/src/components/RHPWrapper/WideRHPWrapper/index.tsx deleted file mode 100644 index 4d7ac74a53334..0000000000000 --- a/src/components/RHPWrapper/WideRHPWrapper/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useContext} from 'react'; -import SecondaryOverlay from '@components/RHPWrapper/SecondaryOverlay'; -import TertiaryOverlay from '@components/RHPWrapper/TertiaryOverlay'; -import {WideRHPContext} from '@components/WideRHPContextProvider'; -import {navigationRef} from '@libs/Navigation/Navigation'; -import NAVIGATORS from '@src/NAVIGATORS'; - -type WideRHPWrapperProps = { - children: React.ReactNode; - shouldShow?: boolean; -}; - -export default function WideRHPWrapper({children, shouldShow}: WideRHPWrapperProps) { - const {syncRHPKeys} = useContext(WideRHPContext); - - // This hook handles the case when a wider RHP is displayed above a narrower one. - // In this situation, we need to synchronize the keys, as superWideRHPKeys and wideRHPKeys store the keys of the screens that are visible. - useFocusEffect( - useCallback( - () => () => { - if (!shouldShow) { - return; - } - - // Synchronization after RHP unmount is handled in RightModalNavigator.tsx. - const isRHPOpened = navigationRef?.getRootState()?.routes?.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; - if (!isRHPOpened) { - return; - } - - syncRHPKeys(); - }, - [shouldShow, syncRHPKeys], - ), - ); - - if (!shouldShow) { - return children; - } - - return ( - <> - {children} - {/* These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen */} - - - - ); -} From 7cf3e27b1f8b50f73cea2f9b15f47e5143fe0adf Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 23 Dec 2025 13:19:06 +0100 Subject: [PATCH 23/38] Fix navigating back from ReportDetailsPage --- src/pages/ReportDetailsPage.tsx | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index fe715f0c83db7..e8d46379be918 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,3 +1,4 @@ +import {StackActions} from '@react-navigation/native'; import reportsSelector from '@selectors/Attributes'; import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -41,9 +42,9 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import getBase62ReportID from '@libs/getBase62ReportID'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; +import type {ReportDetailsNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import Permissions from '@libs/Permissions'; @@ -123,7 +124,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -906,6 +907,31 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail // Only proceed with navigation logic if transaction was actually deleted if (!isEmptyObject(requestParentReportAction)) { + const rootState = navigationRef.getRootState(); + const rhp = rootState.routes.at(-1); + const previousRoute = rhp?.state?.routes?.at(-2); + const secondToLast = rhp?.state?.routes?.at(-3); + + if ( + (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || previousRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) && + (previousRoute.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID === route.params.reportID + ) { + Navigation.dismissModal(); + return; + } + + if ( + previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && + (previousRoute.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_REPORT])?.reportID === route.params.reportID + ) { + if (secondToLast?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || secondToLast?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + navigationRef.dispatch({...StackActions.pop(2), target: rhp?.state?.key}); + return; + } + Navigation.dismissModal(); + return; + } + const isTrackExpense = isTrackExpenseAction(requestParentReportAction); if (isTrackExpense) { urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( @@ -936,7 +962,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail setDeleteTransactionNavigateBackUrl(urlToNavigateBack); navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); } - }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, moneyRequestReport, isChatIOUReportArchived, iouReport, chatIOUReport]); + }, [requestParentReportAction, route.params.reportID, moneyRequestReport, iouTransactionID, iouReport, chatIOUReport, isChatIOUReportArchived, isSingleTransactionView]); // A flag to indicate whether the user chose to delete the transaction or not const isTransactionDeleted = useRef(false); From 793dd5d12bfd449b07937eb21f0b9c524035efa0 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 8 Jan 2026 11:12:57 +0100 Subject: [PATCH 24/38] Fix swrhp/wrhp screens animations --- .../useModalStackScreenOptions.ts | 5 ---- .../Navigators/RightModalNavigator.tsx | 26 ++++++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index 685e72b3fb5a7..877cfc63363a6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -31,11 +31,6 @@ function useWideModalStackScreenOptions() { cardStyleInterpolator = enhanceCardStyleInterpolator(CardStyleInterpolators.forHorizontalIOS, { cardStyle: styles.wideRHPExtendedCardInterpolatorStyles, }); - // single RHPs displayed above the wide RHP need to be positioned - } else if (superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0) { - cardStyleInterpolator = enhanceCardStyleInterpolator(CardStyleInterpolators.forHorizontalIOS, { - cardStyle: styles.singleRHPExtendedCardInterpolatorStyles, - }); } } diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 6124b0c388327..92cbd4148cf27 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -344,14 +344,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS} component={ModalStackNavigators.SearchReportActionsModalStackNavigator} /> - { - const options = modalStackScreenOptions(props); - return {...options, animation: animationEnabledOnSearchReport ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; - }} - /> + { + const options = modalStackScreenOptions(props); + return {...options, animation: animationEnabledOnSearchReport ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; + }} + /> { + const options = modalStackScreenOptions(props); + return {...options, animation: isSmallScreenWidth ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; + }} /> { + const options = modalStackScreenOptions(props); + return {...options, animation: isSmallScreenWidth ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; + }} /> Date: Thu, 8 Jan 2026 12:55:39 +0100 Subject: [PATCH 25/38] Remove InteractionManager from show wide rhp hooks --- .../WideRHPContextProvider/default.ts | 2 ++ src/components/WideRHPContextProvider/index.tsx | 17 ++++++++++++++++- src/components/WideRHPContextProvider/types.ts | 6 ++++++ .../useShowWideRHPVersion/index.ts | 2 ++ .../useModalStackScreenOptions.ts | 5 +++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/components/WideRHPContextProvider/default.ts b/src/components/WideRHPContextProvider/default.ts index 7add883bb4a18..10a359bfcb9cc 100644 --- a/src/components/WideRHPContextProvider/default.ts +++ b/src/components/WideRHPContextProvider/default.ts @@ -20,6 +20,8 @@ const defaultWideRHPContextValue: WideRHPContextType = { removeSuperWideRHPRouteKey: () => {}, syncRHPKeys: () => {}, clearWideRHPKeys: () => {}, + setIsWideRHPClosing: () => {}, + setIsSuperWideRHPClosing: () => {}, }; export default defaultWideRHPContextValue; diff --git a/src/components/WideRHPContextProvider/index.tsx b/src/components/WideRHPContextProvider/index.tsx index 941c3d20b6dff..c697c1a846b40 100644 --- a/src/components/WideRHPContextProvider/index.tsx +++ b/src/components/WideRHPContextProvider/index.tsx @@ -1,5 +1,5 @@ import {findFocusedRoute} from '@react-navigation/native'; -import React, {createContext, useCallback, useEffect, useMemo, useState} from 'react'; +import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react'; // We use Animated for all functionality related to wide RHP to make it easier // to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. // eslint-disable-next-line no-restricted-imports @@ -111,6 +111,17 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: expenseReportSelector, canBeMissing: true}); + const isWideRHPClosingRef = useRef(false); + const isSuperWideRHPClosingRef = useRef(false); + + const setIsWideRHPClosing = useCallback((isClosing: boolean) => { + isWideRHPClosingRef.current = isClosing; + }, []); + + const setIsSuperWideRHPClosing = useCallback((isClosing: boolean) => { + isSuperWideRHPClosingRef.current = isClosing; + }, []); + const {focusedRoute, focusedNavigator} = useRootNavigationState((state) => { if (!state) { return {focusedRoute: undefined, focusedNavigator: undefined}; @@ -333,6 +344,8 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { isSuperWideRHPFocused, syncRHPKeys, clearWideRHPKeys, + setIsWideRHPClosing, + setIsSuperWideRHPClosing, }), [ wideRHPRouteKeys, @@ -354,6 +367,8 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { isSuperWideRHPFocused, syncRHPKeys, clearWideRHPKeys, + setIsWideRHPClosing, + setIsSuperWideRHPClosing, ], ); diff --git a/src/components/WideRHPContextProvider/types.ts b/src/components/WideRHPContextProvider/types.ts index 8ffb8e7dca87e..20300f48287e8 100644 --- a/src/components/WideRHPContextProvider/types.ts +++ b/src/components/WideRHPContextProvider/types.ts @@ -57,6 +57,12 @@ type WideRHPContextType = { // Clear the arrays of wide and super wide rhp keys clearWideRHPKeys: () => void; + + // Set that wide rhp is closing + setIsWideRHPClosing: (isClosing: boolean) => void; + + // Set that super wide rhp is closing + setIsSuperWideRHPClosing: (isClosing: boolean) => void; }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts b/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts index 4adf2fef8f4b7..e7d655a889dd3 100644 --- a/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts +++ b/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts @@ -24,6 +24,8 @@ function useShowWideRHPVersion(condition: boolean) { } }, [removeWideRHPRouteKey, route]); + + /** * Effect that sets up cleanup when the screen is unmounted. */ diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index 877cfc63363a6..685e72b3fb5a7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -31,6 +31,11 @@ function useWideModalStackScreenOptions() { cardStyleInterpolator = enhanceCardStyleInterpolator(CardStyleInterpolators.forHorizontalIOS, { cardStyle: styles.wideRHPExtendedCardInterpolatorStyles, }); + // single RHPs displayed above the wide RHP need to be positioned + } else if (superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0) { + cardStyleInterpolator = enhanceCardStyleInterpolator(CardStyleInterpolators.forHorizontalIOS, { + cardStyle: styles.singleRHPExtendedCardInterpolatorStyles, + }); } } From 2a6291d88c0212fb1450c2623b730aec89e38578 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 9 Jan 2026 12:50:09 +0100 Subject: [PATCH 26/38] fix specific case, when optimistic report is displayed in the background and redirect needed --- src/libs/actions/Report.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f5069d302e49d..59c683231ee6c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -78,7 +78,7 @@ import Log from '@libs/Log'; import {isEmailPublicDomain} from '@libs/LoginUtils'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import enhanceParameters from '@libs/Network/enhanceParameters'; import * as NetworkStore from '@libs/Network/NetworkStore'; import NetworkConnection from '@libs/NetworkConnection'; @@ -182,6 +182,7 @@ import type {OnboardingAccounting} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { BankAccountList, @@ -230,6 +231,7 @@ import {isAnonymousUser} from './Session'; import {onServerDataReady} from './Welcome'; import {getOnboardingMessages} from './Welcome/OnboardingFlow'; import type {OnboardingCompanySize, OnboardingMessage} from './Welcome/OnboardingFlow'; +import SCREENS from '@src/SCREENS'; type SubscriberCallback = (isFromCurrentUser: boolean, reportAction: ReportAction | undefined) => void; @@ -1975,12 +1977,34 @@ function handlePreexistingReport(report: Report) { }); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); }; + + if(!navigationRef.isReady()) { + callback(); + return; + } + + const activeRouteInfo = navigationRef.getCurrentRoute(); + const backTo = (activeRouteInfo?.params as {backTo?: Route})?.backTo; + const screenName = activeRouteInfo?.name; + const activeRoute = activeRouteInfo?.path; + + const isOptimisticReportFocused = activeRoute?.includes(`/r/${reportID}`); + + // Fix specific case: when user is editing a money request report (/e/ route) and has + // an optimistic report in the background that should be replaced with preexisting report + const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`); + // Only re-route them if they are still looking at the optimistically created report - if (Navigation.getActiveRoute().includes(`/r/${reportID}`)) { + if (isOptimisticReportFocused || isOptimisticReportInBackground) { const currCallback = callback; callback = () => { currCallback(); - Navigation.setParams({reportID: preexistingReportID.toString()}); + if (isOptimisticReportFocused) { + Navigation.setParams({reportID: preexistingReportID.toString()}); + } else if (isOptimisticReportInBackground) { + // Navigate to the corrected backTo route with the preexisting report ID + Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route); + } }; // The report screen will listen to this event and transfer the draft comment to the existing report From 405521a8e2aeac7f5ad4e455e5e329f76be36b9a Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 9 Jan 2026 13:13:59 +0100 Subject: [PATCH 27/38] fix comments --- src/libs/actions/Report.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 59c683231ee6c..52847c46d62c9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1990,7 +1990,8 @@ function handlePreexistingReport(report: Report) { const isOptimisticReportFocused = activeRoute?.includes(`/r/${reportID}`); - // Fix specific case: when user is editing a money request report (/e/ route) and has + // Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730. + // When user is editing a money request report (/e/:reportID route) and has // an optimistic report in the background that should be replaced with preexisting report const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`); @@ -2002,7 +2003,7 @@ function handlePreexistingReport(report: Report) { if (isOptimisticReportFocused) { Navigation.setParams({reportID: preexistingReportID.toString()}); } else if (isOptimisticReportInBackground) { - // Navigate to the corrected backTo route with the preexisting report ID + // Navigate to the correct backTo route with the preexisting report ID Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route); } }; From f6e19e3e6f9a1241890f79cd5b959f0f846540d5 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 9 Jan 2026 16:21:02 +0100 Subject: [PATCH 28/38] Disable auto focus on search report when another rhp is displayed below --- .../useShowWideRHPVersion/index.ts | 2 -- src/libs/actions/Report.ts | 4 ++-- .../ComposerWithSuggestions.tsx | 14 +++++++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts b/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts index e7d655a889dd3..4adf2fef8f4b7 100644 --- a/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts +++ b/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts @@ -24,8 +24,6 @@ function useShowWideRHPVersion(condition: boolean) { } }, [removeWideRHPRouteKey, route]); - - /** * Effect that sets up cleanup when the screen is unmounted. */ diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 52847c46d62c9..76504ea7fbff1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -183,6 +183,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { BankAccountList, @@ -231,7 +232,6 @@ import {isAnonymousUser} from './Session'; import {onServerDataReady} from './Welcome'; import {getOnboardingMessages} from './Welcome/OnboardingFlow'; import type {OnboardingCompanySize, OnboardingMessage} from './Welcome/OnboardingFlow'; -import SCREENS from '@src/SCREENS'; type SubscriberCallback = (isFromCurrentUser: boolean, reportAction: ReportAction | undefined) => void; @@ -1978,7 +1978,7 @@ function handlePreexistingReport(report: Report) { Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); }; - if(!navigationRef.isReady()) { + if (!navigationRef.isReady()) { callback(); return; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 0a97361266789..632cec2cf8295 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,7 +1,7 @@ -import {useIsFocused, useNavigation} from '@react-navigation/native'; +import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefObject} from 'react'; -import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {BlurEvent, LayoutChangeEvent, MeasureInWindowOnSuccessCallback, TextInput, TextInputContentSizeChangeEvent, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native'; import {DeviceEventEmitter, InteractionManager, NativeModules, StyleSheet, View} from 'react-native'; import {useFocusedInputHandler} from 'react-native-keyboard-controller'; @@ -11,6 +11,7 @@ import type {Emoji} from '@assets/emojis/types'; import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types'; import Composer from '@components/Composer'; import type {CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types'; +import {WideRHPContext} from '@components/WideRHPContextProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; @@ -49,6 +50,7 @@ import {areAllModalsHidden} from '@userActions/Modal'; import {broadcastUserIsTyping, saveReportActionDraft, saveReportDraftComment} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {FileObject} from '@src/types/utils/Attachment'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -232,6 +234,7 @@ function ComposerWithSuggestions({ // Fullstory forwardedFSClass, }: ComposerWithSuggestionsProps) { + const route = useRoute(); const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); const styles = useThemeStyles(); @@ -255,6 +258,10 @@ function ComposerWithSuggestions({ const commentRef = useRef(value); + const {superWideRHPRouteKeys} = useContext(WideRHPContext); + // Autofocus is disabled on SearchReport when another RHP is displayed below as it causes animation issues + const shouldDisableAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; + const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true}); const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true}); const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED, {canBeMissing: true}); @@ -266,7 +273,8 @@ function ComposerWithSuggestions({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const shouldAutoFocus = (shouldFocusInputOnScreenFocus || !!draftComment) && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput; + const shouldAutoFocus = + (shouldFocusInputOnScreenFocus || !!draftComment) && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput && !shouldDisableAutoFocus; const valueRef = useRef(value); valueRef.current = value; From bfcf0235bbf280a7b8d8858a686bc4a14bcab411 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 9 Jan 2026 17:14:33 +0100 Subject: [PATCH 29/38] fix reject button when dismissing to expense report --- src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts | 6 +----- src/pages/iou/RejectReasonPage.tsx | 5 +++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts b/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts index 71084ffcdb126..8ddf6ba84aa11 100644 --- a/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts +++ b/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts @@ -1,5 +1,5 @@ import type {NavigationState} from '@react-navigation/native'; -import {SUPER_WIDE_RIGHT_MODALS, WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; +import {SUPER_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; import NAVIGATORS from '@src/NAVIGATORS'; const isReportOpenInSuperWideRHP = (state: NavigationState | undefined): boolean => { @@ -7,10 +7,6 @@ const isReportOpenInSuperWideRHP = (state: NavigationState | undefined): boolean if (!lastRoute) { return false; } - const params = lastRoute.params; - if (params && 'screen' in params && typeof params.screen === 'string' && WIDE_RIGHT_MODALS.has(params.screen)) { - return true; - } return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name))); }; diff --git a/src/pages/iou/RejectReasonPage.tsx b/src/pages/iou/RejectReasonPage.tsx index 929cbb58347dd..1068c65e9c0fe 100644 --- a/src/pages/iou/RejectReasonPage.tsx +++ b/src/pages/iou/RejectReasonPage.tsx @@ -5,6 +5,7 @@ import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -32,8 +33,8 @@ function RejectReasonPage({route}: RejectReasonPageProps) { const onSubmit = (values: FormOnyxValues) => { const urlToNavigateBack = rejectMoneyRequest(transactionID, reportID, values.comment, policy); removeTransaction(transactionID); - Navigation.dismissModal(); - if (urlToNavigateBack) { + Navigation.dismissToSuperWideRHP(); + if (urlToNavigateBack && getIsSmallScreenWidth()) { Navigation.isNavigationReady().then(() => Navigation.goBack(urlToNavigateBack)); } }; From 11a43f8ade8004740d7dff05c4c018b24fe673eb Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 12 Jan 2026 11:51:20 +0100 Subject: [PATCH 30/38] Remove redundant RHPReportScreen --- src/pages/home/report/RHPReportScreen.tsx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/pages/home/report/RHPReportScreen.tsx diff --git a/src/pages/home/report/RHPReportScreen.tsx b/src/pages/home/report/RHPReportScreen.tsx deleted file mode 100644 index ea4eb24bb03fc..0000000000000 --- a/src/pages/home/report/RHPReportScreen.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; -import ReportScreen from '@pages/home/ReportScreen'; -import type SCREENS from '@src/SCREENS'; - -type PageProps = PlatformStackScreenProps; - -// When switching search reports using arrows SET_PARAMS action is performed, but it's necessary to remount the whole component after changing the reportID due to the hooks dependencies. -// It's suggested to refactor hooks in ReportScreen to remove this file and perform SET_PARAMS without the report screen remount. -export default function RHPReportScreen({route, navigation}: PageProps) { - return ( - - ); -} From 1139f4d47fa905e1d0052a8665ec1577bcab416b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 13 Jan 2026 09:42:33 +0100 Subject: [PATCH 31/38] Fix navigating after leaving chat from swrhp --- src/libs/actions/Report.ts | 3 +-- src/pages/ReportDetailsPage.tsx | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 76504ea7fbff1..8bd789cffdb01 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3722,8 +3722,7 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { const topmostSuperWideRHP = Navigation.getTopmostSuperWideRHPReportID(); if (lastAccessedReportID === topmostSuperWideRHP && !getIsNarrowLayout()) { - const lastAccessedReportRoute = ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: lastAccessedReportID}); - Navigation.goBack(lastAccessedReportRoute); + Navigation.dismissToSuperWideRHP(); } else { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); Navigation.goBack(lastAccessedReportRoute); diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e8d46379be918..cb38a04803029 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -328,7 +328,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered, isSelfDM]); const leaveChat = useCallback(() => { - Navigation.dismissToSuperWideRHP(); Navigation.isNavigationReady().then(() => { if (isRootGroupChat) { leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID, currentUserPersonalDetails.accountID); From 00a041b051544929e792ec7e2539a9fe61843d09 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 13 Jan 2026 11:50:13 +0100 Subject: [PATCH 32/38] Hide receipt empty state for invoice on narrow layout --- src/components/ReportActionItem/MoneyRequestReceiptView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 9ea62f0f8ea3d..bb8c47f07a559 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -159,8 +159,8 @@ function MoneyRequestReceiptView({ const transactionToCheck = updatedTransaction ?? transaction; const doesTransactionHaveReceipt = !!transactionToCheck?.receipt && !isEmptyObject(transactionToCheck?.receipt); - // @TODO: Verify if we don't need to check isInvoice here - const shouldShowReceiptEmptyState = !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt; + // Empty state for invoices should be displayed only in WideRHP + const shouldShowReceiptEmptyState = (isDisplayedInWideRHP || !isInvoice) && !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt; const [receiptImageViolations, receiptViolations] = useMemo(() => { const imageViolations = []; From c39abbb617d1924e0ce78a73c7561fd81b7487f5 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 13 Jan 2026 12:21:05 +0100 Subject: [PATCH 33/38] Fix issue: Expense - Review duplicates RHP loads infinitely after deleting expense --- src/components/MoneyRequestHeader.tsx | 7 ++++++- src/libs/Navigation/Navigation.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 50808f30e565c..73a130e6637ef 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -99,7 +99,7 @@ type MoneyRequestHeaderProps = { function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPress}: MoneyRequestHeaderProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isSmallScreenWidth, isInNarrowPaneModal} = useResponsiveLayout(); const route = useRoute< PlatformStackRouteProp | PlatformStackRouteProp >(); @@ -570,6 +570,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true); removeTransaction(transaction.transactionID); } + if (isInNarrowPaneModal) { + Navigation.navigateBackToLastSuperWideRHPScreen(); + return; + } + onBackButtonPress(); }} onCancel={() => setIsDeleteModalVisible(false)} diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 111d1822024e9..8166149584f32 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -817,6 +817,10 @@ function dismissToPreviousRHP() { return dismissToModalStack(ALL_WIDE_RIGHT_MODALS); } +function navigateBackToLastSuperWideRHPScreen() { + return dismissToModalStack(SUPER_WIDE_RIGHT_MODALS); +} + function dismissToSuperWideRHP() { // On narrow layouts (mobile), Super Wide RHP doesn't exist, so just dismiss the modal completely if (getIsNarrowLayout()) { @@ -824,7 +828,7 @@ function dismissToSuperWideRHP() { return; } // On wide layouts, dismiss back to the Super Wide RHP modal stack - return dismissToModalStack(SUPER_WIDE_RIGHT_MODALS); + navigateBackToLastSuperWideRHPScreen(); } function getTopmostReportIDInSearchRHP(state = navigationRef.getRootState()): string | undefined { @@ -885,6 +889,7 @@ export default { dismissToSuperWideRHP, getTopmostReportIDInSearchRHP, getTopmostSuperWideRHPReportID, + navigateBackToLastSuperWideRHPScreen, }; export {navigationRef}; From 733910ba0dbce1ce2f345b7711c1db22108d7d18 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 13 Jan 2026 15:17:30 +0100 Subject: [PATCH 34/38] Adjust deleting expenses from ReportDetailsPage --- src/pages/ReportDetailsPage.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index cb38a04803029..8fd0d30879e59 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -23,6 +23,7 @@ import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; +import {SUPER_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; import useActivePolicy from '@hooks/useActivePolicy'; import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -908,23 +909,29 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail if (!isEmptyObject(requestParentReportAction)) { const rootState = navigationRef.getRootState(); const rhp = rootState.routes.at(-1); - const previousRoute = rhp?.state?.routes?.at(-2); - const secondToLast = rhp?.state?.routes?.at(-3); + const rhpRoutes = rhp?.state?.routes ?? []; + const previousRoute = rhpRoutes.at(-2); + const superWideRHPIndex = rhpRoutes.findIndex((rhpRoute) => SUPER_WIDE_RIGHT_MODALS.has(rhpRoute.name)); + // If the deleted expense is displayed directly below, close the entire RHP + const isSuperWideRHPDisplayed = superWideRHPIndex > -1; + const isSuperWideRHPDisplayedDirectlyBelow = isSuperWideRHPDisplayed && superWideRHPIndex === rhpRoutes.length - 2; if ( - (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || previousRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) && - (previousRoute.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID === route.params.reportID + isSuperWideRHPDisplayedDirectlyBelow && + (previousRoute?.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID === route.params.reportID ) { Navigation.dismissModal(); return; } + // If the deleted expense is opened from the super wide rhp, go back there. if ( previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && (previousRoute.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_REPORT])?.reportID === route.params.reportID ) { - if (secondToLast?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || secondToLast?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { - navigationRef.dispatch({...StackActions.pop(2), target: rhp?.state?.key}); + if (isSuperWideRHPDisplayed) { + const distanceToPop = rhpRoutes.length - 1 - superWideRHPIndex; + navigationRef.dispatch({...StackActions.pop(distanceToPop), target: rhp?.state?.key}); return; } Navigation.dismissModal(); From 5e08b39d8bd625824ebe41745e7661b62f2ed289 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 13 Jan 2026 16:21:23 +0100 Subject: [PATCH 35/38] Remove todo comments --- src/components/Navigation/SearchSidebar.tsx | 1 - src/components/ParentNavigationSubtitle.tsx | 1 - src/components/ScrollOffsetContextProvider.tsx | 1 - src/libs/ReportActionComposeFocusManager.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index 3868b387fa2fc..bf9bd4d44505d 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -55,7 +55,6 @@ function SearchSidebar({state}: SearchSidebarProps) { setLastSearchType(currentSearchResults.type); }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.type]); - // @TODO: Check this case const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading; if (shouldUseNarrowLayout) { diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 172754d47e819..e7fde9e2b44c5 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -162,7 +162,6 @@ function ParentNavigationSubtitle({ const previousRoute = currentFocusedNavigator?.state?.routes.at(-2); if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { - // @TODO: Check this case if (previousRoute.params && 'reportID' in previousRoute.params) { const reportIDFromParams = previousRoute.params.reportID; diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index f77009bdd9c02..1c3fbc9052f83 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -108,7 +108,6 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp const routeName = focusedRoute?.name; const isSearchScreen = routeName === SCREENS.SEARCH.ROOT; - // @TODO: Check this case const isSearchMoneyRequestReport = routeName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_REPORT; diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index aee74a59cf149..435468cb8a0e4 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -42,7 +42,6 @@ function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) { /** Do not trigger the refocusing when the active route is not the report screen */ const navigationState = navigationRef.getState(); const focusedRoute = findFocusedRoute(navigationState); - // @TODO: Check this case if (!navigationState || (!isReportOpenInRHP(navigationState) && focusedRoute?.name !== SCREENS.REPORT)) { return; } From dd1b78ee34ef7e25ef6ece86404ead751437dc70 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 13 Jan 2026 16:53:32 +0100 Subject: [PATCH 36/38] Remove isFromRHP param from navigateBackOnDeleteTransaction as it is always true --- src/libs/ReportUtils.ts | 23 ++--------------------- src/libs/actions/IOU/index.ts | 8 +------- src/pages/ReportDetailsPage.tsx | 3 +-- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 53b98fdf444c4..9d3090efe320d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6201,7 +6201,7 @@ function goBackToDetailsPage(report: OnyxEntry, backTo?: string, shouldG } } -function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean) { +function navigateBackOnDeleteTransaction(backRoute: Route | undefined) { if (!backRoute) { return; } @@ -6209,29 +6209,10 @@ function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP const rootState = navigationRef.current?.getRootState(); const lastFullScreenRoute = rootState?.routes.findLast((route) => isFullScreenName(route.name)); if (lastFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { - // @TODO: Migrate this logic to the new navigation structure - // const searchFullScreenRoutes = rootState?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); - // const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1); - // if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - // const lastRouteParams = lastRoute?.params; - // const newBackRoute = lastRouteParams && 'backTo' in lastRouteParams ? lastRouteParams?.backTo : undefined; - // if (isFromRHP) { - // Navigation.dismissModal(); - // } - // Navigation.isNavigationReady().then(() => { - // Navigation.goBack(newBackRoute as Route); - // }); - // return; - // } - // Navigation.dismissModal(); - - // When deleting from search, go back to Super Wide RHP to maintain search context Navigation.dismissToSuperWideRHP(); return; } - if (isFromRHP) { - Navigation.dismissToSuperWideRHP(); - } + Navigation.dismissToSuperWideRHP(); Navigation.isNavigationReady().then(() => { Navigation.goBack(backRoute); }); diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 6f8884e2b6b80..ac83a12874d2f 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -50,7 +50,6 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {getMicroSecondOnyxErrorObject, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {readFileAsync} from '@libs/fileDownload/FileUtils'; import type {MinimalTransaction} from '@libs/Formula'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; import { calculateAmount as calculateIOUAmount, @@ -8637,12 +8636,7 @@ function getNavigationUrlOnMoneyRequestDelete( // Determine which report to navigate back to if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { - // On narrow layouts (mobile), navigate to the full report screen - if (getIsNarrowLayout()) { - return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); - } - // On wide layouts, navigate to the Super Wide RHP version of the expense report - return ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReport.reportID}); + return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); } if (iouReport?.chatReportID && shouldDeleteIOUReport) { diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 8fd0d30879e59..9b25bd252c53e 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -904,7 +904,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail // Where to navigate back to after deleting the transaction and its report. const navigateToTargetUrl = useCallback(() => { let urlToNavigateBack: string | undefined; - // Only proceed with navigation logic if transaction was actually deleted if (!isEmptyObject(requestParentReportAction)) { const rootState = navigationRef.getRootState(); @@ -966,7 +965,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail Navigation.dismissModal(); } else { setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); + navigateBackOnDeleteTransaction(urlToNavigateBack as Route); } }, [requestParentReportAction, route.params.reportID, moneyRequestReport, iouTransactionID, iouReport, chatIOUReport, isChatIOUReportArchived, isSingleTransactionView]); From 7ceccd14ecaf71068c229217e981a42fd25aa735 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 14 Jan 2026 10:40:10 +0100 Subject: [PATCH 37/38] Fix issue: Expense - Report loads infinitely after removing expenses from report offline --- .../MoneyRequestReportView/MoneyRequestReportView.tsx | 2 +- src/libs/MoneyRequestReportUtils.ts | 6 +++++- src/pages/home/ReportScreen.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index c5e2b35a56379..f96ce35ec9f38 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -143,7 +143,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe // Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render // We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate - const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata); + const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata, isOffline); const isEmptyTransactionReport = visibleTransactions && visibleTransactions.length === 0 && transactionThreadReportID === undefined; const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? []); diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts index 264ecb724d63d..bd3c60d3106c7 100644 --- a/src/libs/MoneyRequestReportUtils.ts +++ b/src/libs/MoneyRequestReportUtils.ts @@ -128,7 +128,11 @@ function shouldDisplayReportTableView(report: OnyxEntry, transactions: T return !isReportTransactionThread(report) && !isSingleTransactionReport(report, transactions); } -function shouldWaitForTransactions(report: OnyxEntry, transactions: Transaction[] | undefined, reportMetadata: OnyxEntry) { +function shouldWaitForTransactions(report: OnyxEntry, transactions: Transaction[] | undefined, reportMetadata: OnyxEntry, isOffline = false) { + if (isOffline) { + return false; + } + const isTransactionDataReady = transactions !== undefined; const isTransactionThreadView = isReportTransactionThread(report); const isStillLoadingData = transactions?.length === 0 && ((!!reportMetadata?.isLoadingInitialReportActions && !reportMetadata.hasOnceLoadedReportActions) || report?.total !== 0); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index c7ad26b7ef7e9..967c220613ff3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -357,7 +357,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report); // Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render // We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate - const shouldWaitForTransactions = !isOffline && shouldWaitForTransactionsUtil(report, reportTransactions, reportMetadata); + const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, reportTransactions, reportMetadata, isOffline); const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, reportTransactions); From c0a093fd5953817d40b4d199439a7687aa668ddd Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 14 Jan 2026 15:44:47 +0100 Subject: [PATCH 38/38] Fix new messages button on reports displayed in RHP --- src/libs/Navigation/types.ts | 6 ------ src/pages/home/report/ReportActionsList.tsx | 8 +------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1dec1166cca69..182b2932caabd 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2273,12 +2273,6 @@ type RightModalNavigatorParamList = { }; [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { - reportID: string; - reportActionID?: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; - }; }; type TravelNavigatorParamList = { diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 367db0380bfeb..3463b28b2a247 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -624,13 +624,7 @@ function ReportActionsList({ setIsFloatingMessageCounterVisible(false); if (!hasNewestReportAction) { - if (isSearchTopmostFullScreenRoute()) { - if (Navigation.getReportRHPActiveRoute()) { - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo})); - } else { - Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: report.reportID, backTo})); - } - } else { + if (!Navigation.getReportRHPActiveRoute()) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, backTo)); } openReport(report.reportID);