diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 6c0a5b460654..2f45b4c450a0 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -208,3 +208,274 @@ The action for the first step created with `getMinimalAction` looks like this: ### Deeplinking There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones. + +### Tests + +#### There should be a proper report under attachment screen after reload + +1. Open any report with image attachment on narrow layout. +2. Open attachment. +3. Reload the page. +4. Verify that after pressing back arrow in the header you are on the report where you sent the attachment. + + +#### There is a proper split navigator under RHP with a sidebar screen only for screens that can be opened from the sidebar + +1. Open the browser on narrow layout with url `/settings/profile/status`. +2. Reload the page. +3. Verify that after pressing back arrow in the header you are on the settings root page. + + +#### There is a proper split navigator under the overlay after refreshing page with RHP/LHP on wide screen + +1. Open the browser on wide screen with url `/settings/profile/display-name`. +2. Verify that you can see settings profile page under the overlay of RHP. + + +#### There is a proper split navigator under the overlay after deeplinking to page with RHP/LHP on wide screen + +1. Open the browser on wide screen. +2. Open any report. +3. Send message with url `/settings/profile/display-name`. +4. Press the sent link +5. Verify that the settings profile screen is now visible under the overlay + +#### The Workspace list page is displayed (SCREENS.SETTINGS.WORKSPACES) after clicking the Settings tab from the Workspace settings screen + +1. Open any workspace settings (Settings → Workspaces → Select any workspace) +2. Click the Settings button on the bottom tab. +3. Verify that the Workspace list is displayed (`/settings/workspaces`) +4. Select any workspace again. +5. Reload the page. +6. Click the Settings button on the bottom tab. +7. Verify that the Workspace list is displayed (`/settings/workspaces`) + + +#### The last visited screen in the settings tab is saved when switching between tabs + +1. Open the app. +2. Go to the settings tab. +3. Open the workspace list. +4. Select any workspace. +5. Switch between tabs and open the settings tabs again. +6. Verify that the last visited page in this tab is displayed. + + +#### The Workspace selected in the application is reset when you select a chat that does not belong to the current policy + +1. Open the home page. +2. Click on the Expensify icon in the upper left corner. +3. Select any workspace. +4. Click on the magnifying glass above the list of available chats. +5. Select a chat that does not belong to the workspace selected in the third step. +6. Verify if the chat is opened and the global workspace is selected. + + +#### The selected workspace is saved between Search and Inbox tabs + +1. Open the Inbox tab. +2. Change the workspace using the workspace switcher. +3. Switch to the Search tab and verify if the workspace selected in the second step is also selected in the Search. +4. Change the workspace once again. +5. Go back to the Inbox. +6. Verify if the workspace selected in the fourth step is also selected in the Inbox tab. + +#### Going up to the workspace list page after refreshing on the workspace settings and pressing the up button + +1. Open the workspace settings from the deep link (use a link in format: `/settings/workspaces/:policyID:/profile`) +2. Click the app’s back button. +3. Verify if the workspace list is displayed. + +#### Going up to the RHP screen provided in the backTo parameter in the url + +1. Open the settings tab. +2. Go to the Profile page. +3. Click the Address button. +4. Click the Country button. +5. Reload the page. +6. Click the app’s back button. +7. Verify if the Profile address page is displayed (`/settings/profile/address`) + +#### There is proper split navigator under the overlay after refreshing page in RHP that includes valid reportID in params + +wide layout : + +1. Open any report. +2. Open report details (press the chat header). +3. Reload the app. +4. Verify that the report under the overlay is the same as the one opened in report details. + +narrow layout : + +1. Open any report +2. Open report details (press the chat header). +3. Reload the app. +4. Verify that after pressing back arrow in the header you are on the report previously seen in the details page. + +#### Navigating back to the Workspace Switcher from the created workspace + +1. Open the app and go to the Inbox tab. +2. Open the workspace switcher (Click on the button in the upper left corner). +3. Create a new workspace by clicking on the + button. +4. Navigate back using the back button in the app. +5. Verify if the workspace switcher is displayed with the report screen below it + +#### Going up to the sidebar screen + +Linked issue: https://github.com/Expensify/App/pull/44138 + +1. Go to Subscription page in the settings tab. +2. Click on Request refund button +3. Verify that modal shown +4. Next click Downgrade... +5. Verify that modal got closed, your account is downgraded and the Home page is opened. + +#### Navigating back from the Search page with invalid query parameters + +1. Open the search page with invalid query parameters (e.g `/search?q=from%3a`) +2. Press the app's back button on the not found page. +3. Verify that the Search page with default query parameters is displayed. + +#### Navigating to the chat from the link in the thread + +1. Open any chat. +2. If there are no messages in the chat, send a message. +3. Press reply in thread. +4. Press the "From" link in the displayed header. +5. Verify if the link correctly redirects to the chat opened in the first step. + +#### Expense - App does not open destination report after submitting expense + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2432400819 + +1. Launch the app. +2. Open FAB > Submit expense > Manual. +3. Submit a manual expense to any user (as long as the user is not the currrently opened report and the receiver is not workspace chat). +4. Verify if the destination report is opened after submitting expense. + +#### QBO - Preferred exporter/Export date tab do not auto-close after value selected + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433342220 + +Precondition: Workspace with QBO integration connected. + +1. Go to Workspace > Accounting. +2. Click on Export > Preferred exporter (or Export date). +3. Click on value. +4. Verify if the value chosen in the third step is selected and the app redirects to the Export page. + +#### Web - Hold - App flickers after entering reason and saving it when holding expense + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433389682 + +1. Launch the app. +2. Open DM with any user. +3. Submit two expenses to them. +4. Click on the expense preview to go to expense report. +5. Click on any preview to go to transaction thread. +6. Go back to expense report. +7. Right click on the expense preview in Step 5 > Hold. +8. Enter a reason and save it. +9. Verify if the app does not flicker after entering reason and saving it. + +#### Group - App returns to group settings page after saving group name + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433381800 + +1. Launch the app. +2. Create a group chat. +3. Go to group chat. +4. Click on the group chat header. +5. Click Group name field. +6. Click Save. +7. Verify if the app returs to group details RHP after saving group name. + +#### Going up to a screen with any params + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2432694948 + +1. Press the FAB. +2. Select "Book travel". +3. Press "Book travel" in the new RHP pane. +4. Press "Country". +5. Select any country. +6. Verify that the country you selected is actually visible in the form. + +#### Change params of existing attachments screens instead of pushing new screen on the stack + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2432360626 + +1. Open any chat. +2. Send at least two images. +3. Open attachment by pressing on image. +4. Press arrow on the side of attachment modal to navigate to the second image. +5. Close the modal with X in the corner. +6. Verify that the modal is now fully closed. + +#### Navigate instead of push for reports with same reportID + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433351709 + +1. Open app on wide layout web. +2. Go to report A (any report). +3. Go to report B (any report with message). +4. Press reply in thread. +5. Press on header subtitle. +6. Press on the report B in the sidebar. +7. Verify that the message you replied to is no longer highlighted. +8. Press the browsers back button. +9. Verify that you are on the A report. + + +#### Don't push the default full screen route if not necessary. + +1. Open app on wide layout web. +2. Open search tab. +3. Press track expense. +4. Verify that the split navigator hasn't changed under the overlay. + +#### BA - Back button on connect bank account modal opens incorporation state modal + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433261611 + +Precondition: Use staging server (it can be set in Settings >> Troubleshoot) + +1. Launch the app. +2. Navigate to Settings >> Workspaces >> Workspace >> Workflows. +3. Select Connect with Plaid option. +4. Go through the Plaid flow (Added Wells Fargo details). +5. Complete the Personal info, Company info & agreements section. +6. Note user redirected to page with the header Connect bank account and the option to disconnect your now set up bank account. +7. Tap back button on connect bank account modal. +8. Verify if the connect bank account modal is closed and the Workflows page is opened with the bank account added. + +#### App opens room details page when tapping RHP back button after saving Private notes in DM + +Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433321607 + +1. Launch the app. +2. Open DM with any user that does not have content in Private notes. +3. Click on the chat header. +4. Click Private notes. +5. Enter anything and click Save. +6. Click on the RHP back button. +7. Verify if the Profile RHP Page is opened (URL in the format /a/:accountID). + +#### Opening particular onboarding pages from a link and going back + +Linked issue: https://github.com/Expensify/App/issues/50177 + +1. Sign in as a new user. +2. Select Something else from the onboarding flow. +3. Reopen/refresh the app. +4. Verify the Personal detail step is shown. +5. Go back. +6. Verify you are navigated back to the Purpose step. +7. Select Manage my team. +8. Choose the employee size. +9. Reopen/refresh the app. +10. Verify the connection integration step is shown. +11. Go back. +12. Verify you are navigated back to the employee size step. +13. Go back. +14. Verify you are navigated back to the Purpose step. \ No newline at end of file diff --git a/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch index c65ebbb98007..8cd2a1c2f7f6 100644 --- a/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch +++ b/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch @@ -43,7 +43,7 @@ index 7558eb3..b7bb75e 100644 }) : STATE_TRANSITIONING_OR_BELOW_TOP; } + -+ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Workspace_Initial') && isScreenActive !== STATE_ON_TOP; ++ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Workspace_Initial' || route.name === 'Home' || route.name === 'Search_Bottom_Tab' || route.name === 'Settings_Root' || route.name === 'ReportsSplitNavigator' || route.name === 'Search_Central_Pane') && isScreenActive !== STATE_ON_TOP; + const { headerShown = true, diff --git a/src/App.tsx b/src/App.tsx index 52904e0a06c4..6a4eac7ec7da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,6 @@ import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; import ActiveElementRoleProvider from './components/ActiveElementRoleProvider'; -import ActiveWorkspaceContextProvider from './components/ActiveWorkspaceProvider'; import ColorSchemeWrapper from './components/ColorSchemeWrapper'; import ComposeProviders from './components/ComposeProviders'; import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground'; @@ -34,7 +33,6 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import CONFIG from './CONFIG'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; -import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; @@ -87,8 +85,6 @@ function App({url}: AppProps) { EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, - ActiveWorkspaceContextProvider, - ReportIDsContextProvider, PlaybackContextProvider, FullScreenContextProvider, VolumeContextProvider, diff --git a/src/CONST.ts b/src/CONST.ts index 2d38d26d8820..0ba7cce2d972 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4613,13 +4613,14 @@ const CONST = { SF_COORDINATES: [-122.4194, 37.7749], NAVIGATION: { - TYPE: { - UP: 'UP', - }, ACTION_TYPE: { REPLACE: 'REPLACE', PUSH: 'PUSH', NAVIGATE: 'NAVIGATE', + + /** These action types are custom for RootNavigator */ + SWITCH_POLICY_ID: 'SWITCH_POLICY_ID', + DISMISS_MODAL: 'DISMISS_MODAL', }, }, TIME_PERIOD: { diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index b7c7a71c2828..f3aed5fc175b 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -4,7 +4,6 @@ * */ export default { CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator', - BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator', LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator', @@ -13,4 +12,7 @@ export default { EXPLANATION_MODAL_NAVIGATOR: 'ExplanationModalNavigator', MIGRATED_USER_MODAL_NAVIGATOR: 'MigratedUserModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', + REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator', + SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator', + WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator', } as const; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 47090bd7075b..139040d4c36b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -54,7 +54,6 @@ const SCREENS = { SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP', ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', - BOTTOM_TAB: 'Search_Bottom_Tab', }, SETTINGS: { ROOT: 'Settings_Root', diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx index 466f0f492c8e..8e336a421b92 100644 --- a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx +++ b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx @@ -1,11 +1,8 @@ import {createContext} from 'react'; -type ActiveWorkspaceContextType = { - activeWorkspaceID?: string; - setActiveWorkspaceID: (activeWorkspaceID?: string) => void; -}; - -const ActiveWorkspaceContext = createContext({activeWorkspaceID: undefined, setActiveWorkspaceID: () => undefined}); +const ActiveWorkspaceContext = createContext<{activeWorkspaceID: string | undefined; setActiveWorkspaceID: (workspaceID: string | undefined) => void}>({ + activeWorkspaceID: undefined, + setActiveWorkspaceID: () => {}, +}); export default ActiveWorkspaceContext; -export {type ActiveWorkspaceContextType}; diff --git a/src/components/ActiveWorkspaceProvider/index.tsx b/src/components/ActiveWorkspaceProvider/index.tsx index bc7260cdf10b..0e9a23108483 100644 --- a/src/components/ActiveWorkspaceProvider/index.tsx +++ b/src/components/ActiveWorkspaceProvider/index.tsx @@ -1,9 +1,18 @@ -import React, {useMemo, useState} from 'react'; +import {useNavigationState} from '@react-navigation/native'; +import React, {useEffect, useMemo, useState} from 'react'; import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; +import {getPolicyIDFromState} from '@libs/Navigation/helpers'; +import type {RootStackParamList, State} from '@libs/Navigation/types'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; function ActiveWorkspaceContextProvider({children}: ChildrenProps) { - const [activeWorkspaceID, setActiveWorkspaceID] = useState(undefined); + const policyID = useNavigationState((state) => getPolicyIDFromState(state as State)); + + const [activeWorkspaceID, setActiveWorkspaceID] = useState(policyID); + + useEffect(() => { + setActiveWorkspaceID(policyID); + }, [policyID, setActiveWorkspaceID]); const value = useMemo( () => ({ @@ -17,3 +26,4 @@ function ActiveWorkspaceContextProvider({children}: ChildrenProps) { } export default ActiveWorkspaceContextProvider; +export {}; diff --git a/src/components/ActiveWorkspaceProvider/index.website.tsx b/src/components/ActiveWorkspaceProvider/index.website.tsx deleted file mode 100644 index 82e46d70f896..000000000000 --- a/src/components/ActiveWorkspaceProvider/index.website.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, {useCallback, useMemo, useState} from 'react'; -import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; -import CONST from '@src/CONST'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -function ActiveWorkspaceContextProvider({children}: ChildrenProps) { - const [activeWorkspaceID, updateActiveWorkspaceID] = useState(undefined); - - const setActiveWorkspaceID = useCallback((workspaceID: string | undefined) => { - updateActiveWorkspaceID(workspaceID); - if (workspaceID && sessionStorage) { - sessionStorage?.setItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID, workspaceID); - } else { - sessionStorage?.removeItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID); - } - }, []); - - const value = useMemo( - () => ({ - activeWorkspaceID, - setActiveWorkspaceID, - }), - [activeWorkspaceID, setActiveWorkspaceID], - ); - - return {children}; -} - -export default ActiveWorkspaceContextProvider; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 0ad6dfbb8f7f..a3e9b2ca1d8b 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -290,8 +290,8 @@ function AttachmentModal({ const deleteAndCloseModal = useCallback(() => { IOU.detachReceipt(transaction?.transactionID ?? '-1'); setIsDeleteReceiptConfirmModalVisible(false); - Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1')); - }, [transaction, report]); + Navigation.goBack(); + }, [transaction]); const isValidFile = useCallback( (fileObject: FileObject) => diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx index 73427f0d11aa..9848902a817f 100644 --- a/src/components/DeeplinkWrapper/index.website.tsx +++ b/src/components/DeeplinkWrapper/index.website.tsx @@ -1,9 +1,9 @@ import {Str} from 'expensify-common'; import {useEffect, useRef, useState} from 'react'; import * as Browser from '@libs/Browser'; +import {shouldPreventDeeplinkPrompt} from '@libs/Navigation/helpers'; import Navigation from '@libs/Navigation/Navigation'; import navigationRef from '@libs/Navigation/navigationRef'; -import shouldPreventDeeplinkPrompt from '@libs/Navigation/shouldPreventDeeplinkPrompt'; import * as App from '@userActions/App'; import * as Link from '@userActions/Link'; import * as Session from '@userActions/Session'; diff --git a/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts b/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts deleted file mode 100644 index f6a4f5ba6e83..000000000000 --- a/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts +++ /dev/null @@ -1,6 +0,0 @@ -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; - -const BOTTOM_TAB_SCREENS = [SCREENS.HOME, SCREENS.SETTINGS.ROOT, NAVIGATORS.BOTTOM_TAB_NAVIGATOR, SCREENS.SEARCH.BOTTOM_TAB]; - -export default BOTTOM_TAB_SCREENS; diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index 14f14aee8c73..2a5fb850066c 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -1,11 +1,11 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import FocusTrap from 'focus-trap-react'; import React, {useMemo} from 'react'; -import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import {isSidebarScreenName} from '@libs/Navigation/helpers'; import CONST from '@src/CONST'; import type FocusTrapProps from './FocusTrapProps'; @@ -19,7 +19,7 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) { return focusTrapSettings.active; } // Focus trap can't be active on bottom tab screens because it would block access to the tab bar. - if (BOTTOM_TAB_SCREENS.find((screen) => screen === route.name)) { + if (isSidebarScreenName(route.name)) { return false; } diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index 32e063f03109..b98809a14f7c 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -1,4 +1,3 @@ -import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; /** @@ -6,7 +5,6 @@ import SCREENS from '@src/SCREENS'; * focus trap when rendered on a wide screen to allow navigation between them using the keyboard */ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ - NAVIGATORS.BOTTOM_TAB_NAVIGATOR, SCREENS.HOME, SCREENS.SETTINGS.ROOT, SCREENS.REPORT, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx index 89a9fb21d48f..7dd36d372d96 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx @@ -10,14 +10,12 @@ import Text from '@components/Text'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import type {RootStackParamList, State} from '@libs/Navigation/types'; -import Navigation, {navigationRef} from '@navigation/Navigation'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; +import Navigation from '@navigation/Navigation'; 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 ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MentionReportContext from './MentionReportContext'; @@ -74,9 +72,8 @@ function MentionReportRenderer({style, tnode, TDefaultRenderer, ...defaultRender const {reportID, mentionDisplayText} = mentionDetails; let navigationRoute: Route | undefined = reportID ? ROUTES.REPORT_WITH_ID.getRoute(reportID) : undefined; - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(navigationRef.getRootState() as State); const backTo = Navigation.getActiveRoute(); - if (topmostCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { + if (isSearchTopmostFullScreenRoute()) { navigationRoute = reportID ? ROUTES.SEARCH_REPORT.getRoute({reportID, backTo}) : undefined; } const isCurrentRoomMention = reportID === currentReportIDValue; diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index a6b1374b1c8f..b7f5a2fe914b 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -9,7 +9,7 @@ import useAppState from '@hooks/useAppState'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; -import isSideModalNavigator from '@libs/Navigation/isSideModalNavigator'; +import isSideModalNavigator from '@libs/Navigation/helpers/isSideModalNavigator'; import CONST from '@src/CONST'; import {useSplashScreenStateContext} from '@src/SplashScreenStateContext'; diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 6abf72e9e520..06eabac15e3b 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -49,7 +49,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); if (isVisibleAction && !isOffline) { // Pop the chat report screen before navigating to the linked report action. - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID), true); + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID)); } }} accessibilityLabel={translate('threads.parentNavigationSummary', {reportName, workspaceName})} diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx index e6ce3080ee0a..be4c734608c7 100644 --- a/src/components/PromotedActionsBar.tsx +++ b/src/components/PromotedActionsBar.tsx @@ -5,15 +5,13 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as HeaderUtils from '@libs/HeaderUtils'; import * as Localize from '@libs/Localize'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import type {RootStackParamList, State} from '@libs/Navigation/types'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActions from '@userActions/Report'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type {ReportAction} from '@src/types/onyx'; import type OnyxReport from '@src/types/onyx/Report'; import Button from './Button'; @@ -93,9 +91,8 @@ const PromotedActions = { Navigation.goBack(); } const targetedReportID = reportID ?? reportAction?.childReportID ?? ''; - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(navigationRef.getRootState() as State); - if (topmostCentralPaneRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE && isTextHold) { + if (isSearchTopmostFullScreenRoute() && isTextHold) { ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.REPORT_WITH_ID.getRoute(targetedReportID)); return; } diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 09315bfb8a8e..b8a7e1af618a 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -16,7 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList, RootStackParamList} from '@libs/Navigation/types'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -41,6 +41,9 @@ type ScreenWrapperProps = { /** Returns a function as a child to pass insets to or a node to render without insets */ children: ReactNode | React.FC; + /** Content to display under the offline indicator */ + bottomContent?: ReactNode; + /** A unique ID to find the screen wrapper in tests */ testID: string; @@ -97,7 +100,7 @@ type ScreenWrapperProps = { * * This is required because transitionEnd event doesn't trigger in the testing environment. */ - navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp; + navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp; /** Whether to show offline indicator on wide screens */ shouldShowOfflineIndicatorInWideScreen?: boolean; @@ -136,6 +139,7 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, + bottomContent, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -320,6 +324,7 @@ function ScreenWrapper( )} + {bottomContent} diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index 78d8c5ed61fb..9620da9e72e5 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -2,9 +2,9 @@ import type {ParamListBase} from '@react-navigation/native'; import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import usePrevious from '@hooks/usePrevious'; +import {isSidebarScreenName} from '@libs/Navigation/helpers'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NavigationPartialRoute, State} from '@libs/Navigation/types'; -import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type {PriorityMode} from '@src/types/onyx'; @@ -75,14 +75,11 @@ function ScrollOffsetContextProvider({children, priorityMode}: ScrollOffsetConte }, []); const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback((state) => { - const bottomTabNavigator = state.routes.find((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); - if (bottomTabNavigator?.state && 'routes' in bottomTabNavigator.state) { - const bottomTabNavigatorRoutes = bottomTabNavigator.state.routes; - const scrollOffsetkeysOfExistingScreens = bottomTabNavigatorRoutes.map((route) => getKey(route)); - for (const key of Object.keys(scrollOffsetsRef.current)) { - if (!scrollOffsetkeysOfExistingScreens.includes(key)) { - delete scrollOffsetsRef.current[key]; - } + const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name)); + const scrollOffsetkeysOfExistingScreens = sidebarRoutes.map((route) => getKey(route)); + for (const key of Object.keys(scrollOffsetsRef.current)) { + if (!scrollOffsetkeysOfExistingScreens.includes(key)) { + delete scrollOffsetsRef.current[key]; } } }, []); diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index 7aa7e7305bc8..e9015d4080d3 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,6 +1,6 @@ import React, {useContext, useMemo, useRef, useState} from 'react'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import isSearchTopmostCentralPane from '@navigation/isSearchTopmostCentralPane'; +import {isSearchTopmostFullScreenRoute} from '@libs/Navigation/helpers'; import * as Modal from '@userActions/Modal'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -49,7 +49,7 @@ function SearchRouterContextProvider({children}: ChildrenProps) { // So we need a function that is based on ref to correctly open/close it // When user is on `/search` page we focus the Input instead of showing router const toggleSearch = () => { - const isUserOnSearchPage = isSearchTopmostCentralPane(); + const isUserOnSearchPage = isSearchTopmostFullScreenRoute(); if (isUserOnSearchPage && searchPageInputRef.current) { if (searchPageInputRef.current.isFocused()) { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 07f23a96424d..cdc1ffd8cf77 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -19,7 +19,7 @@ import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import memoize from '@libs/memoize'; -import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; @@ -298,7 +298,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo useEffect( () => () => { - if (isSearchTopmostCentralPane()) { + if (isSearchTopmostFullScreenRoute()) { return; } clearSelectedTransactions(); diff --git a/src/components/withPrepareCentralPaneScreen/index.native.tsx b/src/components/withPrepareCentralPaneScreen/index.native.tsx deleted file mode 100644 index 84ba31cd63fd..000000000000 --- a/src/components/withPrepareCentralPaneScreen/index.native.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type React from 'react'; -import freezeScreenWithLazyLoading from '@libs/freezeScreenWithLazyLoading'; - -/** - * This higher-order function is dependent on the platform. On native platforms, screens that aren't already displayed in the navigation stack should be frozen to prevent unnecessary rendering. - * It's handled this way only on mobile platforms because on the web, more than one screen is displayed in a wide layout, so these screens shouldn't be frozen. - */ -export default function withPrepareCentralPaneScreen(lazyComponent: () => React.ComponentType) { - return freezeScreenWithLazyLoading(lazyComponent); -} diff --git a/src/components/withPrepareCentralPaneScreen/index.tsx b/src/components/withPrepareCentralPaneScreen/index.tsx deleted file mode 100644 index f53368188b3d..000000000000 --- a/src/components/withPrepareCentralPaneScreen/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type React from 'react'; - -/** - * This higher-order function is dependent on the platform. On native platforms, screens that aren't already displayed in the navigation stack should be frozen to prevent unnecessary rendering. - * It's handled this way only on mobile platforms because on the web, more than one screen is displayed in a wide layout, so these screens shouldn't be frozen. - */ -export default function withPrepareCentralPaneScreen(lazyComponent: () => React.ComponentType) { - return lazyComponent; -} diff --git a/src/hooks/useActiveCentralPaneRoute.ts b/src/hooks/useActiveCentralPaneRoute.ts deleted file mode 100644 index 05354e810c3d..000000000000 --- a/src/hooks/useActiveCentralPaneRoute.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {useContext} from 'react'; -import ActiveCentralPaneRouteContext from '@libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext'; -import type {AuthScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types'; - -function useActiveCentralPaneRoute(): NavigationPartialRoute | undefined { - return useContext(ActiveCentralPaneRouteContext); -} - -export default useActiveCentralPaneRoute; diff --git a/src/hooks/useActiveWorkspace.ts b/src/hooks/useActiveWorkspace.ts index cce3c2a4b31f..568f1d73727c 100644 --- a/src/hooks/useActiveWorkspace.ts +++ b/src/hooks/useActiveWorkspace.ts @@ -1,8 +1,7 @@ import {useContext} from 'react'; import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; -import type {ActiveWorkspaceContextType} from '@components/ActiveWorkspace/ActiveWorkspaceContext'; -function useActiveWorkspace(): ActiveWorkspaceContextType { +function useActiveWorkspace(): {activeWorkspaceID: string | undefined; setActiveWorkspaceID: (workspaceID: string | undefined) => void} { return useContext(ActiveWorkspaceContext); } diff --git a/src/hooks/useActiveWorkspaceFromNavigationState.ts b/src/hooks/useActiveWorkspaceFromNavigationState.ts deleted file mode 100644 index 0308ece138a6..000000000000 --- a/src/hooks/useActiveWorkspaceFromNavigationState.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {useNavigationState} from '@react-navigation/native'; -import Log from '@libs/Log'; -import type {BottomTabNavigatorParamList} from '@libs/Navigation/types'; -import SCREENS from '@src/SCREENS'; - -/** - * Get the currently selected policy ID stored in the navigation state. This hook should only be called only from screens in BottomTab. - * Differences between this hook and useActiveWorkspace: - * - useActiveWorkspaceFromNavigationState reads the active workspace id directly from the navigation state, it's a bit slower than useActiveWorkspace and it can be called only from BottomTabScreens. - * It allows to read a value of policyID immediately after the update. - * - useActiveWorkspace allows to read the current policyID anywhere, it's faster because it doesn't require searching in the navigation state. - */ -function useActiveWorkspaceFromNavigationState() { - // The last policyID value is always stored in the last route in BottomTabNavigator. - const activeWorkspaceID = useNavigationState((state) => { - // SCREENS.HOME is a screen located in the BottomTabNavigator, if it's not in state.routeNames it means that this hook was called from a screen in another navigator. - if (!state.routeNames.includes(SCREENS.HOME)) { - Log.warn('useActiveWorkspaceFromNavigationState should be called only from BottomTab screens'); - } - - const lastHomeParams = state.routes.findLast((route) => route.name === SCREENS.HOME)?.params ?? {}; - - if ('policyID' in lastHomeParams) { - return lastHomeParams.policyID; - } - }); - - return activeWorkspaceID; -} - -export default useActiveWorkspaceFromNavigationState; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 7d897d485a78..f8331eade1e0 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -1,10 +1,11 @@ +import type {RouteProp} from '@react-navigation/native'; import {findFocusedRoute} from '@react-navigation/native'; -import React, {memo, useEffect, useRef, useState} from 'react'; +import React, {memo, useEffect, useRef} from 'react'; import {NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx, {withOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener'; +import ActiveWorkspaceContextProvider from '@components/ActiveWorkspaceProvider'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; @@ -12,9 +13,8 @@ import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRout import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import TestToolsModal from '@components/TestToolsModal'; import * as TooltipManager from '@components/Tooltip/EducationalTooltip/TooltipManager'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; -import usePermissions from '@hooks/usePermissions'; +import {ReportIDsContextProvider} from '@hooks/useReportIDs'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -24,17 +24,16 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import Log from '@libs/Log'; import NavBarManager from '@libs/NavBarManager'; import getCurrentUrl from '@libs/Navigation/currentUrl'; +import {isOnboardingFlowName} from '@libs/Navigation/helpers'; +import SIDEBAR_TO_SPLIT from '@libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; import Presentation from '@libs/Navigation/PlatformStackNavigation/navigationOptions/presentation'; -import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types'; -import shouldOpenOnAdminRoom from '@libs/Navigation/shouldOpenOnAdminRoom'; -import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList} from '@libs/Navigation/types'; -import {isOnboardingFlowName} from '@libs/NavigationUtils'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; import NetworkConnection from '@libs/NetworkConnection'; import onyxSubscribe from '@libs/onyxSubscribe'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; -import * as ReportUtils from '@libs/ReportUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import * as SessionUtils from '@libs/SessionUtils'; import ConnectionCompletePage from '@pages/ConnectionCompletePage'; @@ -59,15 +58,10 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; -import beforeRemoveReportOpenedFromSearchRHP from './beforeRemoveReportOpenedFromSearchRHP'; -import CENTRAL_PANE_SCREENS from './CENTRAL_PANE_SCREENS'; import createResponsiveStackNavigator from './createResponsiveStackNavigator'; import defaultScreenOptions from './defaultScreenOptions'; -import hideKeyboardOnSwipe from './hideKeyboardOnSwipe'; -import BottomTabNavigator from './Navigators/BottomTabNavigator'; import ExplanationModalNavigator from './Navigators/ExplanationModalNavigator'; import FeatureTrainingModalNavigator from './Navigators/FeatureTrainingModalNavigator'; -import FullScreenNavigator from './Navigators/FullScreenNavigator'; import LeftModalNavigator from './Navigators/LeftModalNavigator'; import MigratedUserWelcomeModalNavigator from './Navigators/MigratedUserWelcomeModalNavigator'; import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator'; @@ -98,29 +92,10 @@ const loadReportAvatar = () => require('../../../pages/Rep const loadReceiptView = () => require('../../../pages/TransactionReceiptPage').default; const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default; -function getCentralPaneScreenInitialParams(screenName: CentralPaneName, initialReportID?: string): Partial> { - if (screenName === SCREENS.SEARCH.CENTRAL_PANE) { - // Generate default query string with buildSearchQueryString without argument. - return {q: SearchQueryUtils.buildSearchQueryString()}; - } - - if (screenName === SCREENS.REPORT) { - return { - openOnAdminRoom: shouldOpenOnAdminRoom() ? true : undefined, - reportID: initialReportID, - }; - } - - return undefined; -} - -function getCentralPaneScreenListeners(screenName: CentralPaneName) { - if (screenName === SCREENS.REPORT) { - return {beforeRemove: beforeRemoveReportOpenedFromSearchRHP}; - } - - return {}; -} +const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default; +const loadSettingsSplitNavigator = () => require('./Navigators/SettingsSplitNavigator').default; +const loadWorkspaceSplitNavigator = () => require('./Navigators/WorkspaceSplitNavigator').default; +const loadSearchPage = () => require('@pages/Search/SearchPage').default; function initializePusher() { return Pusher.init({ @@ -236,22 +211,10 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const rootNavigatorOptions = useRootNavigatorOptions(); - const {canUseDefaultRooms} = usePermissions(); - const {activeWorkspaceID} = useActiveWorkspace(); const {toggleSearch} = useSearchRouterContext(); const modal = useRef({}); const {isOnboardingCompleted} = useOnboardingFlowRouter(); - const [initialReportID] = useState(() => { - const currentURL = getCurrentUrl(); - const reportIdFromPath = currentURL && new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1); - if (reportIdFromPath) { - return reportIdFromPath; - } - - const initialReport = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID); - return initialReport?.reportID ?? ''; - }); useEffect(() => { NavBarManager.setButtonStyle(theme.navigationBarButtonsStyle); @@ -403,24 +366,51 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); - const CentralPaneScreenOptions: PlatformStackNavigationOptions = { - ...hideKeyboardOnSwipe, - headerShown: false, - title: 'New Expensify', - web: { - // Prevent unnecessary scrolling - cardStyle: styles.cardStyleNavigator, - }, + // Animation is disabled when navigating to the sidebar screen + const getSplitNavigatorOptions = (route: RouteProp) => { + if (!shouldUseNarrowLayout || !route?.params) { + return rootNavigatorOptions.fullScreen; + } + + const screenName = 'screen' in route.params ? route.params.screen : undefined; + + if (!screenName) { + return rootNavigatorOptions.fullScreen; + } + + const animationEnabled = !Object.keys(SIDEBAR_TO_SPLIT).includes(screenName); + + return { + ...rootNavigatorOptions.fullScreen, + animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE, + }; }; return ( - + + {/* This have to be the first navigator in auth screens. */} + getSplitNavigatorOptions(route)} + getComponent={loadReportSplitNavigator} + /> getSplitNavigatorOptions(route)} + getComponent={loadSettingsSplitNavigator} + /> + + getSplitNavigatorOptions(route)} + getComponent={loadWorkspaceSplitNavigator} /> - - {Object.entries(CENTRAL_PANE_SCREENS).map(([screenName, componentGetter]) => { - const centralPaneName = screenName as CentralPaneName; - return ( - - ); - })} diff --git a/src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx b/src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx deleted file mode 100644 index 5bbe2046040a..000000000000 --- a/src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type {CentralPaneName} from '@libs/Navigation/types'; -import withPrepareCentralPaneScreen from '@src/components/withPrepareCentralPaneScreen'; -import SCREENS from '@src/SCREENS'; -import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; - -type Screens = Partial React.ComponentType>>; - -const CENTRAL_PANE_SCREENS = { - [SCREENS.SETTINGS.WORKSPACES]: withPrepareCentralPaneScreen(() => require('../../../pages/workspace/WorkspacesListPage').default), - [SCREENS.SETTINGS.PREFERENCES.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Preferences/PreferencesPage').default), - [SCREENS.SETTINGS.SECURITY]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Security/SecuritySettingsPage').default), - [SCREENS.SETTINGS.PROFILE.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Profile/ProfilePage').default), - [SCREENS.SETTINGS.WALLET.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Wallet/WalletPage').default), - [SCREENS.SETTINGS.ABOUT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/AboutPage/AboutPage').default), - [SCREENS.SETTINGS.TROUBLESHOOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Troubleshoot/TroubleshootPage').default), - [SCREENS.SETTINGS.SAVE_THE_WORLD]: withPrepareCentralPaneScreen(() => require('../../../pages/TeachersUnite/SaveTheWorldPage').default), - [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Subscription/SubscriptionSettingsPage').default), - [SCREENS.SEARCH.CENTRAL_PANE]: withPrepareCentralPaneScreen(() => require('../../../pages/Search/SearchPage').default), - [SCREENS.REPORT]: withPrepareCentralPaneScreen(() => require('../../../pages/home/ReportScreen').default), -} satisfies Screens; - -export default CENTRAL_PANE_SCREENS; diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.native.ts b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.native.ts new file mode 100644 index 000000000000..4cc3caa86cdb --- /dev/null +++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.native.ts @@ -0,0 +1,11 @@ +import type {NavigationState} from '@react-navigation/native'; +import {isFullScreenName} from '@libs/Navigation/helpers'; + +function getIsScreenBlurred(state: NavigationState, currentRouteKey: string) { + // If the screen is one of the last two fullscreen routes in the stack, it is not freezed on native platforms. + // One screen below the main one should not be freezed to allow users to return by swiping left. + const lastTwoFullScreenRoutes = state.routes.filter((route) => isFullScreenName(route.name)).slice(-2); + return !lastTwoFullScreenRoutes.some((route) => route.key === currentRouteKey); +} + +export default getIsScreenBlurred; diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts new file mode 100644 index 000000000000..4ad2ea2c9aa5 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts @@ -0,0 +1,9 @@ +import type {NavigationState} from '@react-navigation/native'; +import {isFullScreenName} from '@libs/Navigation/helpers'; + +function getIsScreenBlurred(state: NavigationState, currentRouteKey: string) { + const lastFullScreenRoute = state.routes.findLast((route) => isFullScreenName(route.name)); + return lastFullScreenRoute?.key !== currentRouteKey; +} + +export default getIsScreenBlurred; diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/index.native.tsx b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.native.tsx new file mode 100644 index 000000000000..bb8b070f4545 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.native.tsx @@ -0,0 +1,28 @@ +import {useNavigation, useRoute} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; +import {Freeze} from 'react-freeze'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import getIsScreenBlurred from './getIsScreenBlurred'; + +type FreezeWrapperProps = ChildrenProps & { + /** Prop to disable freeze */ + keepVisible?: boolean; +}; + +function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) { + const navigation = useNavigation(); + const currentRoute = useRoute(); + + const [isScreenBlurred, setIsScreenBlurred] = useState(false); + + useEffect(() => { + const unsubscribe = navigation.addListener('state', (e) => setIsScreenBlurred(getIsScreenBlurred(e.data.state, currentRoute.key))); + return () => unsubscribe(); + }, [currentRoute.key, navigation]); + + return {children}; +} + +FreezeWrapper.displayName = 'FreezeWrapper'; + +export default FreezeWrapper; diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx new file mode 100644 index 000000000000..98133c3f7796 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx @@ -0,0 +1,35 @@ +import {useNavigation, useRoute} from '@react-navigation/native'; +import React, {useEffect, useLayoutEffect, useState} from 'react'; +import {Freeze} from 'react-freeze'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import getIsScreenBlurred from './getIsScreenBlurred'; + +type FreezeWrapperProps = ChildrenProps & { + /** Prop to disable freeze */ + keepVisible?: boolean; +}; + +function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) { + const navigation = useNavigation(); + const currentRoute = useRoute(); + + const [isScreenBlurred, setIsScreenBlurred] = useState(false); + const [freezed, setFreezed] = useState(false); + + useEffect(() => { + const unsubscribe = navigation.addListener('state', (e) => setIsScreenBlurred(getIsScreenBlurred(e.data.state, currentRoute.key))); + return () => unsubscribe(); + }, [currentRoute.key, navigation]); + + // Decouple the Suspense render task so it won't be interrupted by React's concurrent mode + // and stuck in an infinite loop + useLayoutEffect(() => { + setFreezed(isScreenBlurred && !keepVisible); + }, [isScreenBlurred, keepVisible]); + + return {children}; +} + +FreezeWrapper.displayName = 'FreezeWrapper'; + +export default FreezeWrapper; diff --git a/src/libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext.ts b/src/libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext.ts deleted file mode 100644 index 6f37126584a2..000000000000 --- a/src/libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import type {AuthScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types'; - -const ActiveCentralPaneRouteContext = React.createContext | undefined>(undefined); - -export default ActiveCentralPaneRouteContext; diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx deleted file mode 100644 index 62cceee9f400..000000000000 --- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import {useNavigationState} from '@react-navigation/native'; -import React from 'react'; -import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {BottomTabNavigatorParamList, CentralPaneName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; -import SidebarScreen from '@pages/home/sidebar/SidebarScreen'; -import SearchPageBottomTab from '@pages/Search/SearchPageBottomTab'; -import SCREENS from '@src/SCREENS'; -import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; -import ActiveCentralPaneRouteContext from './ActiveCentralPaneRouteContext'; - -const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default; -const Tab = createCustomBottomTabNavigator(); - -const screenOptions: PlatformStackNavigationOptions = { - headerShown: false, -}; - -function BottomTabNavigator() { - const activeRoute = useNavigationState | undefined>(getTopmostCentralPaneRoute); - return ( - - - - - - - - ); -} - -BottomTabNavigator.displayName = 'BottomTabNavigator'; - -export default BottomTabNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx new file mode 100644 index 000000000000..277e1cda3398 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx @@ -0,0 +1,74 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useRef} from 'react'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import usePermissions from '@hooks/usePermissions'; +import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator'; +import FreezeWrapper from '@libs/Navigation/AppNavigator/FreezeWrapper'; +import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions'; +import getCurrentUrl from '@libs/Navigation/currentUrl'; +import shouldOpenOnAdminRoom from '@libs/Navigation/helpers/shouldOpenOnAdminRoom'; +import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import SCREENS from '@src/SCREENS'; +import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; + +const loadReportScreen = () => require('../../../../pages/home/ReportScreen').default; +const loadSidebarScreen = () => require('@pages/home/sidebar/SidebarScreen').default; + +const Split = createSplitNavigator(); + +function ReportsSplitNavigator() { + const {canUseDefaultRooms} = usePermissions(); + const {activeWorkspaceID} = useActiveWorkspace(); + const rootNavigatorOptions = useRootNavigatorOptions(); + const route = useRoute(); + let initialReportID: string | undefined; + const isInitialRender = useRef(true); + + // TODO: Figure out if compiler affects this code. + // eslint-disable-next-line react-compiler/react-compiler + if (isInitialRender.current) { + const currentURL = getCurrentUrl(); + if (currentURL) { + initialReportID = new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1); + } + + if (!initialReportID) { + const initialReport = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID); + initialReportID = initialReport?.reportID ?? ''; + } + + // eslint-disable-next-line react-compiler/react-compiler + isInitialRender.current = false; + } + + return ( + + + + + + + + + ); +} + +ReportsSplitNavigator.displayName = 'ReportsSplitNavigator'; + +export default ReportsSplitNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx new file mode 100644 index 000000000000..e55285986773 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx @@ -0,0 +1,71 @@ +import {useRoute} from '@react-navigation/native'; +import React from 'react'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator'; +import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions'; +import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; +import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; +import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; + +const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default; + +type Screens = Partial React.ComponentType>>; + +const CENTRAL_PANE_SETTINGS_SCREENS = { + [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../pages/workspace/WorkspacesListPage').default, + [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../../pages/settings/Preferences/PreferencesPage').default, + [SCREENS.SETTINGS.SECURITY]: () => require('../../../../pages/settings/Security/SecuritySettingsPage').default, + [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../../pages/settings/Profile/ProfilePage').default, + [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../../pages/settings/Wallet/WalletPage').default, + [SCREENS.SETTINGS.ABOUT]: () => require('../../../../pages/settings/AboutPage/AboutPage').default, + [SCREENS.SETTINGS.TROUBLESHOOT]: () => require('../../../../pages/settings/Troubleshoot/TroubleshootPage').default, + [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: () => require('../../../../pages/settings/Subscription/SubscriptionSettingsPage').default, +} satisfies Screens; + +const Split = createSplitNavigator(); + +function SettingsSplitNavigator() { + const route = useRoute(); + const rootNavigatorOptions = useRootNavigatorOptions(); + + return ( + + + + {Object.entries(CENTRAL_PANE_SETTINGS_SCREENS).map(([screenName, componentGetter]) => { + const options: PlatformStackNavigationOptions = {animation: undefined}; + + if (screenName === SCREENS.SETTINGS.WORKSPACES) { + options.animation = Animations.NONE; + } + + return ( + + ); + })} + + + ); +} + +SettingsSplitNavigator.displayName = 'SettingsSplitNavigator'; + +export {CENTRAL_PANE_SETTINGS_SCREENS}; +export default SettingsSplitNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx similarity index 64% rename from src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx rename to src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 86c9bce765b7..2d269e88ba16 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -1,19 +1,15 @@ +import {useRoute} from '@react-navigation/native'; import React from 'react'; -import {View} from 'react-native'; import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; -import createCustomFullScreenNavigator from '@libs/Navigation/AppNavigator/createCustomFullScreenNavigator'; +import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator'; import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; -const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default; - -const RootStack = createCustomFullScreenNavigator(); +type Screens = Partial React.ComponentType>>; -type Screens = Partial React.ComponentType>>; +const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default; const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default, @@ -33,34 +29,38 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, } satisfies Screens; -function FullScreenNavigator() { - const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); +const Split = createSplitNavigator(); + +function WorkspaceNavigator() { + const route = useRoute(); const rootNavigatorOptions = useRootNavigatorOptions(); return ( - - - + + {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => ( + - {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => ( - - ))} - - + ))} + ); } -FullScreenNavigator.displayName = 'FullScreenNavigator'; +WorkspaceNavigator.displayName = 'WorkspaceNavigator'; export {CENTRAL_PANE_WORKSPACE_SCREENS}; -export default FullScreenNavigator; +export default WorkspaceNavigator; diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx index dbbb11c978d5..f64c98d11cba 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx +++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx @@ -20,9 +20,10 @@ const RootStack = createPlatformStackNavigator(); function PublicScreens() { return ( - {/* The structure for the HOME route has to be the same in public and auth screens. That's why the name for SignInPage is BOTTOM_TAB_NAVIGATOR. */} + {/* The structure for the HOME route has to be the same in public and auth screens. That's why the name for SignInPage is REPORTS_SPLIT_NAVIGATOR. */} ) { - if (!navigationRef.current) { - return; - } - - const state = navigationRef.current?.getRootState() as State; - - if (!state) { - return; - } - - const shouldPopHome = - state.routes?.length >= 3 && - state.routes.at(-1)?.name === SCREENS.REPORT && - state.routes.at(-2)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && - state.routes.at(-3)?.name === SCREENS.SEARCH.CENTRAL_PANE && - getTopmostBottomTabRoute(state)?.name === SCREENS.HOME; - - if (!shouldPopHome) { - return; - } - - event.preventDefault(); - const bottomTabState = state?.routes?.at(0)?.state; - navigationRef.dispatch({...StackActions.pop(), target: bottomTabState?.key}); - Navigation.goBack(); -} - -export default beforeRemoveReportOpenedFromSearchRHP; diff --git a/src/libs/Navigation/AppNavigator/beforeRemoveReportOpenedFromSearchRHP/index.ts b/src/libs/Navigation/AppNavigator/beforeRemoveReportOpenedFromSearchRHP/index.ts deleted file mode 100644 index b5d8f835ab43..000000000000 --- a/src/libs/Navigation/AppNavigator/beforeRemoveReportOpenedFromSearchRHP/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** The issue fixed by this handler is related to navigating back on native platforms. For more information, see the index.native.ts file in this folder */ -function beforeRemoveReportOpenedFromSearchRHP() {} - -export default beforeRemoveReportOpenedFromSearchRHP; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 88a735884493..6ad404d6fddd 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -24,7 +24,6 @@ import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFl import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import DebugTabView from './DebugTabView'; @@ -88,12 +87,12 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { if (selectedTab === SCREENS.HOME) { return; } - const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/${ROUTES.HOME}` as Route) : ROUTES.HOME; - Navigation.navigate(route); - }, [activeWorkspaceID, selectedTab]); + + Navigation.navigate(ROUTES.HOME); + }, [selectedTab]); const navigateToSearch = useCallback(() => { - if (selectedTab === SCREENS.SEARCH.BOTTOM_TAB) { + if (selectedTab === SCREENS.SEARCH.CENTRAL_PANE) { return; } interceptAnonymousUser(() => { @@ -170,7 +169,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -180,7 +179,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { styles.textSmall, styles.textAlignCenter, styles.mt1Half, - selectedTab === SCREENS.SEARCH.BOTTOM_TAB ? styles.textBold : styles.textSupporting, + selectedTab === SCREENS.SEARCH.CENTRAL_PANE ? styles.textBold : styles.textSupporting, styles.bottomTabBarLabel, ]} > diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabNavigationContentWrapper.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabNavigationContentWrapper.tsx deleted file mode 100644 index dd93a6df7b1e..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabNavigationContentWrapper.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type {NavigationContentWrapperProps} from '@libs/Navigation/PlatformStackNavigation/types'; - -function BottomTabNavigationContentWrapper({children, displayName}: NavigationContentWrapperProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -export default BottomTabNavigationContentWrapper; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 01caa79692f1..0a81cf899aff 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -33,6 +33,7 @@ type TopBarProps = { function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false, onSwitchWorkspace}: TopBarProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); const isAnonymousUser = Session.isAnonymousUser(session); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx deleted file mode 100644 index a4e50aeb6516..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type {ParamListBase} from '@react-navigation/native'; -import {createNavigatorFactory} from '@react-navigation/native'; -import React from 'react'; -import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; -import type {ExtraContentProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; -import BottomTabBar from './BottomTabBar'; -import BottomTabNavigationContentWrapper from './BottomTabNavigationContentWrapper'; -import useCustomState from './useCustomState'; - -const defaultScreenOptions: PlatformStackNavigationOptions = { - animation: 'none', -}; - -function ExtraContent({state}: ExtraContentProps) { - const selectedTab = state.routes.at(-1)?.name; - return ; -} - -const CustomBottomTabNavigatorComponent = createPlatformStackNavigatorComponent('CustomBottomTabNavigator', { - useCustomState, - defaultScreenOptions, - NavigationContentWrapper: BottomTabNavigationContentWrapper, - ExtraContent, -}); - -function createCustomBottomTabNavigator() { - return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof CustomBottomTabNavigatorComponent>( - CustomBottomTabNavigatorComponent, - )(); -} - -export default createCustomBottomTabNavigator; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/useCustomState.ts b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/useCustomState.ts deleted file mode 100644 index cf8ffd81840f..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/useCustomState.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {useMemo} from 'react'; -import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {NavigationStateRoute} from '@libs/Navigation/types'; -import SCREENS from '@src/SCREENS'; - -function useCustomState({state}: CustomStateHookProps) { - return useMemo(() => { - const routesToRender = [state.routes.at(-1)] as NavigationStateRoute[]; - - // We need to render at least one HOME screen to make sure everything load properly. This may be not necessary after changing how IS_SIDEBAR_LOADED is handled. - // Currently this value will be switched only after the first HOME screen is rendered. - if (routesToRender.at(0)?.name !== SCREENS.HOME) { - const routeToRender = state.routes.find((route) => route.name === SCREENS.HOME); - if (routeToRender) { - routesToRender.unshift(routeToRender); - } - } - - return {stateToRender: {...state, routes: routesToRender, index: routesToRender.length - 1}}; - }, [state]); -} - -export default useCustomState; diff --git a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/CustomFullScreenRouter.tsx b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/CustomFullScreenRouter.tsx deleted file mode 100644 index ba8de1f298bd..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/CustomFullScreenRouter.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type {ParamListBase, PartialState, RouterConfigOptions} from '@react-navigation/native'; -import {StackRouter} from '@react-navigation/native'; -import Onyx from 'react-native-onyx'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -import type {PlatformStackNavigationState, PlatformStackRouterOptions} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; - -type StackState = PlatformStackNavigationState | PartialState>; - -const isAtLeastOneInState = (state: StackState, screenName: string): boolean => state.routes.some((route) => route.name === screenName); - -let isLoadingReportData = true; -Onyx.connect({ - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - initWithStoredValues: false, - callback: (value) => (isLoadingReportData = value ?? false), -}); - -function adaptStateIfNecessary(state: StackState) { - const isNarrowLayout = getIsNarrowLayout(); - const workspaceCentralPane = state.routes.at(-1); - const policyID = - workspaceCentralPane?.params && 'policyID' in workspaceCentralPane.params && typeof workspaceCentralPane.params.policyID === 'string' - ? workspaceCentralPane.params.policyID - : undefined; - const policy = PolicyUtils.getPolicy(policyID ?? ''); - const isPolicyAccessible = PolicyUtils.isPolicyAccessible(policy); - - // There should always be WORKSPACE.INITIAL screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings. - // The only exception is when the workspace is invalid or inaccessible. - if (!isAtLeastOneInState(state, SCREENS.WORKSPACE.INITIAL)) { - if (isNarrowLayout && !isLoadingReportData && !isPolicyAccessible) { - return; - } - // @ts-expect-error Updating read only property - // noinspection JSConstantReassignment - state.stale = true; // eslint-disable-line - - // This is necessary for ts to narrow type down to PartialState. - if (state.stale === true) { - // Unshift the root screen to fill left pane. - state.routes.unshift({ - name: SCREENS.WORKSPACE.INITIAL, - params: workspaceCentralPane?.params, - }); - } - } - - // If the screen is wide, there should be at least two screens inside: - // - WORKSPACE.INITIAL to cover left pane. - // - WORKSPACE.PROFILE (first workspace settings screen) to cover central pane. - if (!isNarrowLayout) { - if (state.routes.length === 1 && state.routes.at(0)?.name === SCREENS.WORKSPACE.INITIAL) { - // @ts-expect-error Updating read only property - // noinspection JSConstantReassignment - state.stale = true; // eslint-disable-line - // Push the default settings central pane screen. - if (state.stale === true) { - state.routes.push({ - name: SCREENS.WORKSPACE.PROFILE, - params: state.routes.at(0)?.params, - }); - } - } - // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style - (state.index as number) = state.routes.length - 1; - } -} - -function CustomFullScreenRouter(options: PlatformStackRouterOptions) { - const stackRouter = StackRouter(options); - - return { - ...stackRouter, - getInitialState({routeNames, routeParamList, routeGetIdList}: RouterConfigOptions) { - const initialState = stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList}); - adaptStateIfNecessary(initialState); - - // If we needed to modify the state we need to rehydrate it to get keys for new routes. - if (initialState.stale) { - return stackRouter.getRehydratedState(initialState, {routeNames, routeParamList, routeGetIdList}); - } - - return initialState; - }, - getRehydratedState(partialState: StackState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): PlatformStackNavigationState { - adaptStateIfNecessary(partialState); - const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); - return state; - }, - }; -} - -export default CustomFullScreenRouter; diff --git a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx deleted file mode 100644 index f3d605e1824f..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type {ParamListBase} from '@react-navigation/native'; -import {createNavigatorFactory} from '@react-navigation/native'; -import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; -import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; -import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; -import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; -import CustomFullScreenRouter from './CustomFullScreenRouter'; - -const CustomFullScreenNavigatorComponent = createPlatformStackNavigatorComponent('CustomFullScreenNavigator', { - createRouter: CustomFullScreenRouter, - useCustomEffects: useNavigationResetOnLayoutChange, - defaultScreenOptions: defaultPlatformStackScreenOptions, -}); - -function createCustomFullScreenNavigator() { - return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof CustomFullScreenNavigatorComponent>( - CustomFullScreenNavigatorComponent, - )(); -} - -export default createCustomFullScreenNavigator; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts index 842ad5c86854..15aa6b0e9d14 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts @@ -1,108 +1,27 @@ import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; -import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native'; +import {findFocusedRoute, StackRouter} from '@react-navigation/native'; import type {ParamListBase} from '@react-navigation/routers'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; import * as Localize from '@libs/Localize'; -import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import linkingConfig from '@libs/Navigation/linkingConfig'; -import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath'; -import type {PlatformStackRouterOptions} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; -import {isCentralPaneName, isOnboardingFlowName} from '@libs/NavigationUtils'; +import {isOnboardingFlowName, isSideModalNavigator} from '@libs/Navigation/helpers'; import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; +import * as GetStateForActionHandlers from './GetStateForActionHandlers'; import syncBrowserHistory from './syncBrowserHistory'; +import type {CustomRouterAction, CustomRouterActionType, DismissModalActionType, PushActionType, ResponsiveStackNavigatorRouterOptions, SwitchPolicyIdActionType} from './types'; -function insertRootRoute(state: State, routeToInsert: NavigationPartialRoute) { - const nonModalRoutes = state.routes.filter( - (route) => route.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR && route.name !== NAVIGATORS.LEFT_MODAL_NAVIGATOR && route.name !== NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR, - ); - const modalRoutes = state.routes.filter( - (route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || route.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR || route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR, - ); - - // It's safe to modify this state before returning in getRehydratedState. - - // @ts-expect-error Updating read only property - // noinspection JSConstantReassignment - state.routes = [...nonModalRoutes, routeToInsert, ...modalRoutes]; // eslint-disable-line - - // @ts-expect-error Updating read only property - // noinspection JSConstantReassignment - state.index = state.routes.length - 1; // eslint-disable-line - - // @ts-expect-error Updating read only property - // noinspection JSConstantReassignment - state.stale = true; // eslint-disable-line +function isSwitchPolicyIdAction(action: CustomRouterAction): action is SwitchPolicyIdActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; } -function compareAndAdaptState(state: StackNavigationState) { - // If the state of the last path is not defined the getPathFromState won't work correctly. - if (!state?.routes.at(-1)?.state) { - return; - } +function isPushAction(action: CustomRouterAction): action is PushActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.PUSH; +} - // We need to be sure that the bottom tab state is defined. - const topmostBottomTabRoute = getTopmostBottomTabRoute(state); - const isNarrowLayout = getIsNarrowLayout(); - - // This solutions is heuristics and will work for our cases. We may need to improve it in the future if we will have more cases to handle. - if (topmostBottomTabRoute && !isNarrowLayout) { - const fullScreenRoute = state.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); - - // If there is fullScreenRoute we don't need to add anything. - if (fullScreenRoute) { - return; - } - - // We will generate a template state and compare the current state with it. - // If there is a difference in the screens that should be visible under the overlay, we will add the screen from templateState to the current state. - const pathFromCurrentState = getPathFromState(state, linkingConfig.config); - const {adaptedState: templateState} = getAdaptedStateFromPath(pathFromCurrentState, linkingConfig.config); - - if (!templateState) { - return; - } - - const templateFullScreenRoute = templateState.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); - - // If templateFullScreenRoute is defined, and full screen route is not in the state, we need to add it. - if (templateFullScreenRoute) { - insertRootRoute(state, templateFullScreenRoute); - return; - } - - const topmostCentralPaneRoute = state.routes.filter((route) => isCentralPaneName(route.name)).at(-1); - const templateCentralPaneRoute = templateState.routes.find((route) => isCentralPaneName(route.name)); - - const topmostCentralPaneRouteExtracted = getTopmostCentralPaneRoute(state); - const templateCentralPaneRouteExtracted = getTopmostCentralPaneRoute(templateState as State); - - // If there is no templateCentralPaneRoute, we don't have anything to add. - if (!templateCentralPaneRoute) { - return; - } - - // If there is no topmostCentralPaneRoute in the state and template state has one, we need to add it. - if (!topmostCentralPaneRoute) { - insertRootRoute(state, templateCentralPaneRoute); - return; - } - - // If there is central pane route in state and template state has one, we need to check if they are the same. - if (topmostCentralPaneRouteExtracted && templateCentralPaneRouteExtracted && topmostCentralPaneRouteExtracted.name !== templateCentralPaneRouteExtracted.name) { - // Not every RHP screen has matching central pane defined. In that case we use the REPORT screen as default for initial screen. - // But we don't want to override the central pane for those screens as they may be opened with different central panes under the overlay. - // e.g. i-know-a-teacher may be opened with different central panes under the overlay - if (templateCentralPaneRouteExtracted.name === SCREENS.REPORT) { - return; - } - insertRootRoute(state, templateCentralPaneRoute); - } - } +function isDismissModalAction(action: CustomRouterAction): action is DismissModalActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL; } function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) { @@ -121,24 +40,62 @@ function shouldPreventReset(state: StackNavigationState, action: return false; } -function CustomRouter(options: PlatformStackRouterOptions) { +function isNavigatingToModalFromModal(state: StackNavigationState, action: CommonActions.Action | StackActionType) { + if (action.type !== CONST.NAVIGATION.ACTION_TYPE.PUSH) { + return false; + } + + const lastRoute = state.routes.at(-1); + + // If the last route is a side modal navigator and the generated minimal action want's to push a new side modal navigator that means they are different ones. + // We want to dismiss the one that is currently on the top. + if (isSideModalNavigator(lastRoute?.name) && isSideModalNavigator(action.payload.name)) { + return true; + } + return false; +} + +function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { const stackRouter = StackRouter(options); + const {setActiveWorkspaceID} = useActiveWorkspace(); + // @TODO: Make sure that everything works fine without compareAndAdaptState function. Probably with getMatchingFullScreenRoute. return { ...stackRouter, - getRehydratedState(partialState: StackNavigationState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState { - compareAndAdaptState(partialState); - const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); - return state; - }, - getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) { + getStateForAction(state: StackNavigationState, action: CustomRouterAction, configOptions: RouterConfigOptions) { + if (isSwitchPolicyIdAction(action)) { + return GetStateForActionHandlers.handleSwitchPolicyID(state, action, configOptions, stackRouter, setActiveWorkspaceID); + } + + if (isDismissModalAction(action)) { + return GetStateForActionHandlers.handleDismissModalAction(state, configOptions, stackRouter); + } + + if (isPushAction(action)) { + if (action.payload.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) { + return GetStateForActionHandlers.handlePushReportAction(state, action, configOptions, stackRouter, setActiveWorkspaceID); + } + + if (action.payload.name === SCREENS.SEARCH.CENTRAL_PANE) { + return GetStateForActionHandlers.handlePushSearchPageAction(state, action, configOptions, stackRouter, setActiveWorkspaceID); + } + } + + // Don't let the user navigate back to a non-onboarding screen if they are currently on an onboarding screen and it's not finished. if (shouldPreventReset(state, action)) { syncBrowserHistory(state); return state; } + + if (isNavigatingToModalFromModal(state, action)) { + const modifiedState = {...state, routes: state.routes.slice(0, -1), index: state.index !== 0 ? state.index - 1 : 0}; + return stackRouter.getStateForAction(modifiedState, action, configOptions); + } + return stackRouter.getStateForAction(state, action, configOptions); }, }; } export default CustomRouter; +export type {CustomRouterActionType}; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts new file mode 100644 index 000000000000..1a952c0c6caa --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts @@ -0,0 +1,154 @@ +import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {StackActions} from '@react-navigation/native'; +import type {ParamListBase, Router} from '@react-navigation/routers'; +import Log from '@libs/Log'; +import getPolicyIDFromState from '@libs/Navigation/helpers/getPolicyIDFromState'; +import type {RootStackParamList, State} from '@libs/Navigation/types'; +import * as SearchQueryUtils from '@libs/SearchQueryUtils'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import type {PushActionType, SwitchPolicyIdActionType} from './types'; + +const MODAL_ROUTES_TO_DISMISS: string[] = [ + NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, + NAVIGATORS.LEFT_MODAL_NAVIGATOR, + NAVIGATORS.RIGHT_MODAL_NAVIGATOR, + NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR, + NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR, + SCREENS.NOT_FOUND, + SCREENS.ATTACHMENTS, + SCREENS.TRANSACTION_RECEIPT, + SCREENS.PROFILE_AVATAR, + SCREENS.WORKSPACE_AVATAR, + SCREENS.REPORT_AVATAR, + SCREENS.CONCIERGE, +]; + +function handleSwitchPolicyID( + state: StackNavigationState, + action: SwitchPolicyIdActionType, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, + setActiveWorkspaceID: (workspaceID: string | undefined) => void, +) { + const lastRoute = state.routes.at(-1); + if (lastRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { + const currentParams = lastRoute.params as RootStackParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; + const queryJSON = SearchQueryUtils.buildSearchQueryJSON(currentParams.q); + if (!queryJSON) { + return null; + } + + if (action.payload.policyID) { + queryJSON.policyID = action.payload.policyID; + } else { + delete queryJSON.policyID; + } + + const newAction = StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, { + ...currentParams, + q: SearchQueryUtils.buildSearchQueryString(queryJSON), + }); + + setActiveWorkspaceID(action.payload.policyID); + return stackRouter.getStateForAction(state, newAction, configOptions); + } + if (lastRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) { + const newAction = StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {policyID: action.payload.policyID}); + + setActiveWorkspaceID(action.payload.policyID); + return stackRouter.getStateForAction(state, newAction, configOptions); + } + + // We don't have other navigators that should handle switch policy action. + return null; +} + +function handlePushReportAction( + state: StackNavigationState, + action: PushActionType, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, + setActiveWorkspaceID: (workspaceID: string | undefined) => void, +) { + const haveParamsPolicyID = action.payload.params && 'policyID' in action.payload.params; + let policyID; + + if (haveParamsPolicyID) { + policyID = (action.payload.params as Record)?.policyID; + setActiveWorkspaceID(policyID); + } else { + policyID = getPolicyIDFromState(state as State); + } + + const modifiedAction = { + ...action, + payload: { + ...action.payload, + params: { + ...action.payload.params, + policyID, + }, + }, + }; + + return stackRouter.getStateForAction(state, modifiedAction, configOptions); +} + +function handlePushSearchPageAction( + state: StackNavigationState, + action: PushActionType, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, + setActiveWorkspaceID: (workspaceID: string | undefined) => void, +) { + const currentParams = action.payload.params as RootStackParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; + const queryJSON = SearchQueryUtils.buildSearchQueryJSON(currentParams.q); + + if (!queryJSON) { + return null; + } + + if (!queryJSON.policyID) { + const policyID = getPolicyIDFromState(state as State); + + if (policyID) { + queryJSON.policyID = policyID; + } else { + delete queryJSON.policyID; + } + } else { + setActiveWorkspaceID(queryJSON.policyID); + } + + const modifiedAction = { + ...action, + payload: { + ...action.payload, + params: { + ...action.payload.params, + q: SearchQueryUtils.buildSearchQueryString(queryJSON), + }, + }, + }; + + return stackRouter.getStateForAction(state, modifiedAction, configOptions); +} + +function handleDismissModalAction( + state: StackNavigationState, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, +) { + const lastRoute = state.routes.at(-1); + const newAction = StackActions.pop(); + + if (!lastRoute?.name || !MODAL_ROUTES_TO_DISMISS.includes(lastRoute?.name)) { + Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss'); + return null; + } + + return stackRouter.getStateForAction(state, newAction, configOptions); +} + +export {handleDismissModalAction, handlePushReportAction, handlePushSearchPageAction, handleSwitchPolicyID}; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx deleted file mode 100644 index 2455587660ab..000000000000 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type {ExtraContentProps} from '@libs/Navigation/PlatformStackNavigation/types'; - -function SearchRoute({searchRoute, descriptors}: ExtraContentProps) { - const styles = useThemeStyles(); - - if (!searchRoute) { - return null; - } - - const key = searchRoute.key; - const descriptor = descriptors[key]; - - if (!descriptor) { - return null; - } - - return {descriptor.render()}; -} - -export default SearchRoute; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx index 9ac2ffd6c8f9..5662b394339c 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx @@ -1,19 +1,24 @@ import type {ParamListBase} from '@react-navigation/native'; import {createNavigatorFactory} from '@react-navigation/native'; -import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange'; +import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; +import {isFullScreenName} from '@libs/Navigation/helpers'; import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; -import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {CustomStateHookProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; import CustomRouter from './CustomRouter'; -import RenderSearchRoute from './SearchRoute'; -import useStateWithSearch from './useStateWithSearch'; + +function useCustomRouterState({state}: CustomStateHookProps) { + const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); + const routesToRender = state.routes.slice(Math.max(0, lastSplitIndex - 1), state.routes.length); + + return {...state, routes: routesToRender, index: routesToRender.length - 1}; +} const ResponsiveStackNavigatorComponent = createPlatformStackNavigatorComponent('ResponsiveStackNavigator', { createRouter: CustomRouter, defaultScreenOptions: defaultPlatformStackScreenOptions, - useCustomState: useStateWithSearch, - useCustomEffects: useNavigationResetRootOnLayoutChange, - ExtraContent: RenderSearchRoute, + useCustomEffects: useNavigationResetOnLayoutChange, + useCustomState: useCustomRouterState, }); function createResponsiveStackNavigator() { diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts new file mode 100644 index 000000000000..9f19ede081cd --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts @@ -0,0 +1,44 @@ +import type {CommonActions, DefaultNavigatorOptions, ParamListBase, StackActionType, StackNavigationState, StackRouterOptions} from '@react-navigation/native'; +import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; +import type CONST from '@src/CONST'; + +type CustomRouterActionType = + | { + type: typeof CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; + payload: { + policyID: string; + }; + } + | {type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}; + +type SwitchPolicyIdActionType = CustomRouterActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; +}; + +type PushActionType = StackActionType & {type: typeof CONST.NAVIGATION.ACTION_TYPE.PUSH}; + +type DismissModalActionType = CustomRouterActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL; +}; + +type ResponsiveStackNavigatorConfig = { + isSmallScreenWidth: boolean; +}; + +type ResponsiveStackNavigatorRouterOptions = StackRouterOptions; + +type ResponsiveStackNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & + ResponsiveStackNavigatorConfig; + +type CustomRouterAction = CommonActions.Action | StackActionType | CustomRouterActionType; + +export type { + SwitchPolicyIdActionType, + PushActionType, + DismissModalActionType, + CustomRouterAction, + CustomRouterActionType, + ResponsiveStackNavigatorRouterOptions, + ResponsiveStackNavigatorProps, + ResponsiveStackNavigatorConfig, +}; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/useStateWithSearch.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/useStateWithSearch.ts deleted file mode 100644 index 73984af34d2e..000000000000 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/useStateWithSearch.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type {ParamListBase} from '@react-navigation/native'; -import {useMemo} from 'react'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import type {CustomStateHookProps, PlatformStackNavigationState, PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {RootStackParamList, State} from '@libs/Navigation/types'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import SCREENS from '@src/SCREENS'; - -type Routes = PlatformStackNavigationState['routes']; -function reduceCentralPaneRoutes(routes: Routes): Routes { - const result: Routes = []; - let count = 0; - const reverseRoutes = [...routes].reverse(); - - reverseRoutes.forEach((route) => { - if (isCentralPaneName(route.name)) { - // Remove all central pane routes except the last 3. This will improve performance. - if (count < 3) { - result.push(route); - count++; - } - } else { - result.push(route); - } - }); - - return result.reverse(); -} - -function useStateWithSearch({state}: CustomStateHookProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); - return useMemo(() => { - const routes = reduceCentralPaneRoutes(state.routes); - - if (shouldUseNarrowLayout) { - const isSearchCentralPane = (route: PlatformStackRouteProp) => - getTopmostCentralPaneRoute({routes: [route]} as State)?.name === SCREENS.SEARCH.CENTRAL_PANE; - - const lastRoute = routes.at(-1); - const lastSearchCentralPane = lastRoute && isSearchCentralPane(lastRoute) ? lastRoute : undefined; - const filteredRoutes = routes.filter((route) => !isSearchCentralPane(route)); - - // On narrow layout, if we are on /search route we want to hide all central pane routes and show only the bottom tab navigator. - if (lastSearchCentralPane) { - const filteredRoute = filteredRoutes.at(0); - if (filteredRoute) { - return { - stateToRender: { - ...state, - index: 0, - routes: [filteredRoute], - }, - searchRoute: lastSearchCentralPane, - }; - } - } - - return { - stateToRender: { - ...state, - index: filteredRoutes.length - 1, - routes: filteredRoutes, - }, - searchRoute: undefined, - }; - } - - return { - stateToRender: { - ...state, - index: routes.length - 1, - routes: [...routes], - }, - searchRoute: undefined, - }; - }, [state, shouldUseNarrowLayout]); -} - -export default useStateWithSearch; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts new file mode 100644 index 000000000000..454b13f59417 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -0,0 +1,137 @@ +import type {CommonActions, ParamListBase, PartialState, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {StackActions, StackRouter} from '@react-navigation/native'; +import pick from 'lodash/pick'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {getParamsFromRoute} from '@libs/Navigation/helpers'; +import navigationRef from '@libs/Navigation/navigationRef'; +import SCREENS from '@src/SCREENS'; +import type {SplitNavigatorRouterOptions} from './types'; +import {getPreservedSplitNavigatorState} from './usePreserveSplitNavigatorState'; + +type StackState = StackNavigationState | PartialState>; + +const isAtLeastOneInState = (state: StackState, screenName: string): boolean => state.routes.some((route) => route.name === screenName); + +type AdaptStateIfNecessaryArgs = { + state: StackState; + options: SplitNavigatorRouterOptions; +}; + +function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralScreen, parentRoute}}: AdaptStateIfNecessaryArgs) { + const isNarrowLayout = getIsNarrowLayout(); + + const lastRoute = state.routes.at(-1); + + // If the screen is wide, there should be at least two screens inside: + // - sidebarScreen to cover left pane. + // - defaultCentralScreen to cover central pane. + if (!isAtLeastOneInState(state, sidebarScreen) && !isNarrowLayout) { + const paramsFromRoute = getParamsFromRoute(sidebarScreen); + let params = pick(lastRoute?.params, paramsFromRoute); + + // On a wide screen the backTo param has to be passed to the sidebar screen (SCREENS.WORKSPACE.INITIAL), because the back action is performed from this page + if (lastRoute?.name === SCREENS.WORKSPACE.PROFILE) { + const hasRouteBackToParam = lastRoute?.params && 'backTo' in lastRoute.params; + + if (hasRouteBackToParam) { + params = {...params, backTo: lastRoute.params.backTo}; + } + } + + // @ts-expect-error Updating read only property + // noinspection JSConstantReassignment + state.stale = true; // eslint-disable-line + + // This is necessary for typescript to narrow type down to PartialState. + if (state.stale === true) { + // Unshift the root screen to fill left pane. + state.routes.unshift({ + name: sidebarScreen, + // This handles the case where the sidebar should have params included in the central screen e.g. policyID for workspace initial. + params, + }); + } + } + + // If the screen is wide, there should be at least two screens inside: + // - sidebarScreen to cover left pane. + // - defaultCentralScreen to cover central pane. + if (!isNarrowLayout) { + if (state.routes.length === 1 && state.routes[0].name === sidebarScreen) { + const rootState = navigationRef.getRootState(); + + const previousSameNavigator = rootState?.routes.filter((route) => route.name === parentRoute.name).at(-2); + + // If we have optimization for not rendering all split navigators, then last selected option may not be in the state. In this case state has to be read from the preserved state. + const previousSameNavigatorState = previousSameNavigator?.state ?? (previousSameNavigator?.key ? getPreservedSplitNavigatorState(previousSameNavigator.key) : undefined); + const previousSelectedCentralScreen = + previousSameNavigatorState?.routes && previousSameNavigatorState.routes.length > 1 ? previousSameNavigatorState.routes.at(-1)?.name : undefined; + + // @ts-expect-error Updating read only property + // noinspection JSConstantReassignment + state.stale = true; // eslint-disable-line + // Push the default settings central pane screen. + if (state.stale === true) { + state.routes.push({ + name: previousSelectedCentralScreen ?? defaultCentralScreen, + params: state.routes.at(0)?.params, + }); + } + } + // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style + (state.index as number) = state.routes.length - 1; + } +} + +function isPushingSidebarOnCentralPane(state: StackState, action: CommonActions.Action | StackActionType, options: SplitNavigatorRouterOptions) { + if (action.type === 'PUSH' && action.payload.name === options.sidebarScreen && state.routes.length > 1) { + return true; + } + return false; +} + +function SplitRouter(options: SplitNavigatorRouterOptions) { + const stackRouter = StackRouter(options); + return { + ...stackRouter, + getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) { + if (isPushingSidebarOnCentralPane(state, action, options)) { + if (getIsNarrowLayout()) { + // @TODO: It's possible that it's better to push whole new SplitNavigator in such case. Not sure yet. + const newAction = StackActions.popToTop(); + return stackRouter.getStateForAction(state, newAction, configOptions); + } + // On wide screen do nothing as we want to keep the central pane screen and the sidebar is visible. + return state; + } + return stackRouter.getStateForAction(state, action, configOptions); + }, + getInitialState({routeNames, routeParamList, routeGetIdList}: RouterConfigOptions) { + const preservedState = getPreservedSplitNavigatorState(options.parentRoute.key); + const initialState = preservedState ?? stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList}); + + adaptStateIfNecessary({ + state: initialState, + options, + }); + + // If we needed to modify the state we need to rehydrate it to get keys for new routes. + if (initialState.stale) { + return stackRouter.getRehydratedState(initialState, {routeNames, routeParamList, routeGetIdList}); + } + + return initialState; + }, + getRehydratedState(partialState: StackState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState { + adaptStateIfNecessary({ + state: partialState, + options, + }); + + const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); + return state; + }, + }; +} + +export default SplitRouter; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState.ts new file mode 100644 index 000000000000..65ca59f9d8db --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState.ts @@ -0,0 +1,29 @@ +import type {NavigationState, PartialState} from '@react-navigation/native'; +import SIDEBAR_TO_SPLIT from '@libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT'; +import type {NavigationPartialRoute, SplitNavigatorBySidebar, SplitNavigatorParamListType, SplitNavigatorSidebarScreen} from '@libs/Navigation/types'; + +type ExtractRouteType = Extract; + +// The function getPathFromState that we are using in some places isn't working correctly without defined index. +const getRoutesWithIndex = (routes: NavigationPartialRoute[]): PartialState => ({routes, index: routes.length - 1}); + +function getInitialSplitNavigatorState( + splitNavigatorSidebarRoute: NavigationPartialRoute, + route?: NavigationPartialRoute>, + splitNavigatorParams?: Record, +): NavigationPartialRoute> { + const routes = []; + + routes.push(splitNavigatorSidebarRoute); + + if (route) { + routes.push(route); + } + return { + name: SIDEBAR_TO_SPLIT[splitNavigatorSidebarRoute.name], + state: getRoutesWithIndex(routes), + params: splitNavigatorParams, + }; +} + +export default getInitialSplitNavigatorState; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx new file mode 100644 index 000000000000..3351b0b5333d --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx @@ -0,0 +1,50 @@ +import type {ParamListBase} from '@react-navigation/native'; +import {createNavigatorFactory} from '@react-navigation/native'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; +import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; +import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; +import type { + CustomEffectsHookProps, + CustomStateHookProps, + PlatformStackNavigationEventMap, + PlatformStackNavigationOptions, + PlatformStackNavigationState, +} from '@libs/Navigation/PlatformStackNavigation/types'; +import SplitRouter from './SplitRouter'; +import usePreserveSplitNavigatorState from './usePreserveSplitNavigatorState'; + +function useCustomEffects(props: CustomEffectsHookProps) { + useNavigationResetOnLayoutChange(props); + usePreserveSplitNavigatorState(props.state, props.parentRoute); +} + +function useCustomSplitNavigatorState({state}: CustomStateHookProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const sidebarScreenRoute = state.routes.at(0); + + if (!sidebarScreenRoute) { + return state; + } + + const centralScreenRoutes = state.routes.slice(1); + const routesToRender = shouldUseNarrowLayout ? state.routes.slice(-2) : [sidebarScreenRoute, ...centralScreenRoutes.slice(-2)]; + + return {...state, routes: routesToRender, index: routesToRender.length - 1}; +} + +const CustomFullScreenNavigatorComponent = createPlatformStackNavigatorComponent('CustomFullScreenNavigator', { + createRouter: SplitRouter, + useCustomEffects, + defaultScreenOptions: defaultPlatformStackScreenOptions, + useCustomState: useCustomSplitNavigatorState, +}); + +function createCustomFullScreenNavigator() { + return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof CustomFullScreenNavigatorComponent>( + CustomFullScreenNavigatorComponent, + )(); +} + +export default createCustomFullScreenNavigator; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/types.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/types.ts new file mode 100644 index 000000000000..36da86e8f51a --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/types.ts @@ -0,0 +1,11 @@ +import type {DefaultNavigatorOptions, ParamListBase, RouteProp, StackNavigationState, StackRouterOptions} from '@react-navigation/native'; +import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; + +type SplitNavigatorRouterOptions = StackRouterOptions & {defaultCentralScreen: string; sidebarScreen: string; parentRoute: RouteProp}; + +type SplitNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & { + defaultCentralScreen: Extract; + sidebarScreen: Extract; +}; + +export type {SplitNavigatorProps, SplitNavigatorRouterOptions}; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/useHandleScreenResize/index.native.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/useHandleScreenResize/index.native.ts new file mode 100644 index 000000000000..4ac0a1ae5595 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/useHandleScreenResize/index.native.ts @@ -0,0 +1 @@ +export default function useHandleScreenResize() {} diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/useHandleScreenResize/index.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/useHandleScreenResize/index.ts new file mode 100644 index 000000000000..e6ae505cb560 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/useHandleScreenResize/index.ts @@ -0,0 +1,17 @@ +import type {NavigationHelpers, ParamListBase} from '@react-navigation/native'; +import {useEffect} from 'react'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import navigationRef from '@libs/Navigation/navigationRef'; + +export default function useHandleScreenResize(navigation: NavigationHelpers) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + useEffect(() => { + if (!navigationRef.isReady()) { + return; + } + // We need to separately reset state of this navigator to trigger getRehydratedState. + navigation.reset(navigation.getState()); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [shouldUseNarrowLayout]); +} diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/usePrepareSplitStackNavigatorChildren.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePrepareSplitStackNavigatorChildren.ts new file mode 100644 index 000000000000..4995f96bc1a4 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePrepareSplitStackNavigatorChildren.ts @@ -0,0 +1,31 @@ +import type {EventMapBase, NavigationState, ParamListBase, RouteConfig} from '@react-navigation/native'; +import type {StackNavigationOptions} from '@react-navigation/stack'; +import {Children, isValidElement, useMemo} from 'react'; +import type {ReactNode} from 'react'; + +export default function usePrepareSplitNavigatorChildren(screensNode: ReactNode, sidebarScreenName: string, sidebarScreenOptions: StackNavigationOptions) { + return useMemo( + () => + Children.toArray(screensNode).map((screen: ReactNode) => { + if (!isValidElement(screen)) { + return screen; + } + + // @TODO: Fix types here + const screenProps = screen?.props as RouteConfig, EventMapBase>; + + if (screenProps?.name === sidebarScreenName) { + // If we found the element we wanted, clone it with the provided prop changes. + return { + ...screen, + props: { + ...screenProps, + options: {...sidebarScreenOptions, ...screenProps.options}, + }, + }; + } + return screen; + }), + [screensNode, sidebarScreenName, sidebarScreenOptions], + ); +} diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState.ts new file mode 100644 index 000000000000..789fc27d81fe --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState.ts @@ -0,0 +1,29 @@ +import type {NavigationState, ParamListBase, RouteProp, StackNavigationState} from '@react-navigation/native'; +import {useEffect} from 'react'; + +const preservedSplitNavigatorStates: Record> = {}; + +const cleanPreservedSplitNavigatorStates = (state: NavigationState) => { + const currentSplitNavigatorKeys = state.routes.map((route) => route.key); + + for (const key of Object.keys(preservedSplitNavigatorStates)) { + if (!currentSplitNavigatorKeys.includes(key)) { + delete preservedSplitNavigatorStates[key]; + } + } +}; + +const getPreservedSplitNavigatorState = (key: string) => preservedSplitNavigatorStates[key]; + +function usePreserveSplitNavigatorState(state: StackNavigationState, route: RouteProp | undefined) { + useEffect(() => { + if (!route) { + return; + } + preservedSplitNavigatorStates[route.key] = state; + }, [route, state]); +} + +export default usePreserveSplitNavigatorState; + +export {getPreservedSplitNavigatorState, cleanPreservedSplitNavigatorStates}; diff --git a/src/libs/Navigation/AppNavigator/getActionsFromPartialDiff.ts b/src/libs/Navigation/AppNavigator/getActionsFromPartialDiff.ts deleted file mode 100644 index 9685f4fc0339..000000000000 --- a/src/libs/Navigation/AppNavigator/getActionsFromPartialDiff.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {getActionFromState, StackActions} from '@react-navigation/native'; -import type {NavigationAction} from '@react-navigation/native'; -import linkingConfig from '@libs/Navigation/linkingConfig'; -import NAVIGATORS from '@src/NAVIGATORS'; -import type {GetPartialStateDiffReturnType} from './getPartialStateDiff'; - -/** - * @param diff - Diff generated by getPartialDiff. - * @returns Array of actions to dispatch to apply diff. - */ -function getActionsFromPartialDiff(diff: GetPartialStateDiffReturnType): NavigationAction[] { - const actions: NavigationAction[] = []; - - const bottomTabDiff = diff[NAVIGATORS.BOTTOM_TAB_NAVIGATOR]; - const centralPaneDiff = diff[NAVIGATORS.CENTRAL_PANE_NAVIGATOR]; - const fullScreenDiff = diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR]; - - // There is only one bottom tab navigator so we can just push this route. - if (bottomTabDiff) { - actions.push(StackActions.push(bottomTabDiff.name, bottomTabDiff.params)); - } - - if (centralPaneDiff) { - // In this case we have to wrap the inner central pane route with central pane navigator. - actions.push(StackActions.push(centralPaneDiff.name, centralPaneDiff.params)); - } - - if (fullScreenDiff) { - const action = getActionFromState({routes: [fullScreenDiff]}, linkingConfig.config); - if (action) { - actions.push(action); - } - } - - return actions; -} - -export default getActionsFromPartialDiff; diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts deleted file mode 100644 index 17a8ee158219..000000000000 --- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts +++ /dev/null @@ -1,86 +0,0 @@ -import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import getTopmostFullScreenRoute from '@libs/Navigation/getTopmostFullScreenRoute'; -import type {Metainfo} from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath'; -import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; -import shallowCompare from '@libs/ObjectUtils'; -import NAVIGATORS from '@src/NAVIGATORS'; - -type GetPartialStateDiffReturnType = { - [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]?: NavigationPartialRoute; - [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]?: NavigationPartialRoute; - [NAVIGATORS.FULL_SCREEN_NAVIGATOR]?: NavigationPartialRoute; -}; - -/** - * This function returns partial additive diff between the two states. - * - * Example: Let's start with state A on route /r/123. If the screen is wide we will have a HOME opened on bottom tab and REPORT on central pane. - * Now let's say we want to navigate to /workspace/345/profile. We will generate state B from this path. - * State B will have WORKSPACE_INITIAL on the bottom tab and WORKSPACE_PROFILE on the central pane. - * Now we will generate partial diff between state A and state B. The diff will tell us that we need to push WORKSPACE_INITIAL on the bottom tab and WORKSPACE_PROFILE on the central pane. - * - * Then we can generate actions from this diff and dispatch them to the linkTo function. - * - * It's named partial diff because we don't cover RHP and LHP navigators yet. In the future we can improve this function to handle all navigators to help us clean and simplify the linkTo function. - * - * The partial diff has information which bottom tab, central pane and full screen screens we need to push to go from state to templateState. - * @param state - Current state. - * @param templateState - Desired state generated with getAdaptedStateFromPath. - * @param metainfo - Additional info from getAdaptedStateFromPath function. - * @returns The screen options object - */ -function getPartialStateDiff(state: State, templateState: State, metainfo: Metainfo): GetPartialStateDiffReturnType { - const diff: GetPartialStateDiffReturnType = {}; - - // If it is mandatory we need to compare both central pane and bottom tab of states. - if (metainfo.isCentralPaneAndBottomTabMandatory) { - const stateTopmostBottomTab = getTopmostBottomTabRoute(state); - const templateStateTopmostBottomTab = getTopmostBottomTabRoute(templateState); - - // Bottom tab navigator - if (stateTopmostBottomTab && templateStateTopmostBottomTab && stateTopmostBottomTab.name !== templateStateTopmostBottomTab.name) { - diff[NAVIGATORS.BOTTOM_TAB_NAVIGATOR] = templateStateTopmostBottomTab; - } - - const stateTopmostCentralPane = getTopmostCentralPaneRoute(state); - const templateStateTopmostCentralPane = getTopmostCentralPaneRoute(templateState); - - if ( - // If the central pane is only in the template state, it's diff. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (!stateTopmostCentralPane && templateStateTopmostCentralPane) || - (stateTopmostCentralPane && - templateStateTopmostCentralPane && - stateTopmostCentralPane.name !== templateStateTopmostCentralPane.name && - !shallowCompare(stateTopmostCentralPane.params as Record | undefined, templateStateTopmostCentralPane.params as Record | undefined)) - ) { - // We need to wrap central pane routes in the central pane navigator. - diff[NAVIGATORS.CENTRAL_PANE_NAVIGATOR] = templateStateTopmostCentralPane; - } - } - - // This one is heuristic and may need to be improved if we will be able to navigate from modal screen with full screen in background to another modal screen with full screen in background. - // For now this simple check is enough. - if (metainfo.isFullScreenNavigatorMandatory) { - const stateTopmostFullScreen = getTopmostFullScreenRoute(state); - const templateStateTopmostFullScreen = getTopmostFullScreenRoute(templateState); - const fullScreenDiff = templateState.routes.filter((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR).at(-1) as NavigationPartialRoute; - - if ( - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (!stateTopmostFullScreen && templateStateTopmostFullScreen) || - (stateTopmostFullScreen && - templateStateTopmostFullScreen && - (stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name || - !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined))) - ) { - diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR] = fullScreenDiff; - } - } - - return diff; -} - -export default getPartialStateDiff; -export type {GetPartialStateDiffReturnType}; diff --git a/src/libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange.ts b/src/libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange.ts deleted file mode 100644 index 03caac57410f..000000000000 --- a/src/libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {useEffect} from 'react'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import navigationRef from '@libs/Navigation/navigationRef'; - -/** - * This hook resets the navigation root state when changing the layout size, resetting the state calls the getRehydredState method in CustomRouter.ts. - * When the screen size is changed, it is necessary to check whether the application displays the content correctly. - * When the app is opened on a small layout and the user resizes it to wide, a second screen has to be present in the navigation state to fill the space. - */ -function useNavigationResetRootOnLayoutChange() { - const {shouldUseNarrowLayout} = useResponsiveLayout(); - - useEffect(() => { - if (!navigationRef.isReady()) { - return; - } - navigationRef.resetRoot(navigationRef.getRootState()); - }, [shouldUseNarrowLayout]); -} - -export default useNavigationResetRootOnLayoutChange; diff --git a/src/libs/Navigation/AppNavigator/useRootNavigatorOptions.ts b/src/libs/Navigation/AppNavigator/useRootNavigatorOptions.ts index 27b1c6d2fae1..a2d609eb5d4b 100644 --- a/src/libs/Navigation/AppNavigator/useRootNavigatorOptions.ts +++ b/src/libs/Navigation/AppNavigator/useRootNavigatorOptions.ts @@ -18,6 +18,7 @@ type RootNavigatorOptions = { fullScreen: PlatformStackNavigationOptions; centralPaneNavigator: PlatformStackNavigationOptions; bottomTab: PlatformStackNavigationOptions; + searchPage: PlatformStackNavigationOptions; }; const commonScreenOptions: PlatformStackNavigationOptions = { @@ -109,7 +110,19 @@ const useRootNavigatorOptions = () => { fullScreen: { ...commonScreenOptions, // We need to turn off animation for the full screen to avoid delay when closing screens. - animation: shouldUseNarrowLayout ? Animations.SLIDE_FROM_RIGHT : Animations.NONE, + animation: Animations.NONE, + web: { + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true}), + cardStyle: { + ...StyleUtils.getNavigationModalCardStyle(), + }, + }, + }, + + searchPage: { + ...commonScreenOptions, + // We need to turn off animation for the full screen to avoid delay when closing screens. + animation: Animations.NONE, web: { cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true}), cardStyle: { diff --git a/src/libs/Navigation/FreezeWrapper/index.native.tsx b/src/libs/Navigation/FreezeWrapper/index.native.tsx deleted file mode 100644 index b071a065bd31..000000000000 --- a/src/libs/Navigation/FreezeWrapper/index.native.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; -import React, {useEffect, useRef, useState} from 'react'; -import {Freeze} from 'react-freeze'; -import shouldSetScreenBlurred from '@libs/Navigation/shouldSetScreenBlurred'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type FreezeWrapperProps = ChildrenProps & { - /** Prop to disable freeze */ - keepVisible?: boolean; -}; - -function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) { - const [isScreenBlurred, setIsScreenBlurred] = useState(false); - // we need to know the screen index to determine if the screen can be frozen - const screenIndexRef = useRef(null); - const isFocused = useIsFocused(); - const navigation = useNavigation(); - const currentRoute = useRoute(); - - useEffect(() => { - const index = navigation.getState()?.routes.findIndex((route) => route.key === currentRoute.key) ?? 0; - screenIndexRef.current = index; - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const unsubscribe = navigation.addListener('state', () => { - const navigationIndex = (navigation.getState()?.index ?? 0) - (screenIndexRef.current ?? 0); - setIsScreenBlurred(shouldSetScreenBlurred(navigationIndex)); - }); - return () => unsubscribe(); - }, [isFocused, isScreenBlurred, navigation]); - - return {children}; -} - -FreezeWrapper.displayName = 'FreezeWrapper'; - -export default FreezeWrapper; diff --git a/src/libs/Navigation/FreezeWrapper/index.tsx b/src/libs/Navigation/FreezeWrapper/index.tsx deleted file mode 100644 index 7219666b1b18..000000000000 --- a/src/libs/Navigation/FreezeWrapper/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; -import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'; -import {Freeze} from 'react-freeze'; -import shouldSetScreenBlurred from '@libs/Navigation/shouldSetScreenBlurred'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type FreezeWrapperProps = ChildrenProps & { - /** Prop to disable freeze */ - keepVisible?: boolean; -}; - -function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) { - const [isScreenBlurred, setIsScreenBlurred] = useState(false); - const [freezed, setFreezed] = useState(false); - // we need to know the screen index to determine if the screen can be frozen - const screenIndexRef = useRef(null); - const isFocused = useIsFocused(); - const navigation = useNavigation(); - const currentRoute = useRoute(); - - useEffect(() => { - const index = navigation.getState()?.routes.findIndex((route) => route.key === currentRoute.key) ?? 0; - screenIndexRef.current = index; - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const unsubscribe = navigation.addListener('state', () => { - const navigationIndex = (navigation.getState()?.index ?? 0) - (screenIndexRef.current ?? 0); - setIsScreenBlurred(shouldSetScreenBlurred(navigationIndex)); - }); - return () => unsubscribe(); - }, [isFocused, isScreenBlurred, navigation]); - - // Decouple the Suspense render task so it won't be interuptted by React's concurrent mode - // and stuck in an infinite loop - useLayoutEffect(() => { - setFreezed(!isFocused && isScreenBlurred && !keepVisible); - }, [isFocused, isScreenBlurred, keepVisible]); - - return {children}; -} - -FreezeWrapper.displayName = 'FreezeWrapper'; - -export default FreezeWrapper; diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index d54668bf3f69..4d4370a3ab9d 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,9 +1,14 @@ -import {findFocusedRoute} from '@react-navigation/core'; -import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native'; +import {getActionFromState} from '@react-navigation/core'; +import type {EventArg, NavigationAction, NavigationContainerEventMap} from '@react-navigation/native'; import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native'; +// eslint-disable-next-line you-dont-need-lodash-underscore/omit +import omit from 'lodash/omit'; import type {OnyxEntry} from 'react-native-onyx'; +import type {Writable} from 'type-fest'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import {isCentralPaneName, removePolicyIDParamFromState} from '@libs/NavigationUtils'; +import {shallowCompare} from '@libs/ObjectUtils'; +import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -11,24 +16,25 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridAppRoute, Route} from '@src/ROUTES'; import ROUTES, {HYBRID_APP_ROUTES} from '@src/ROUTES'; -import {PROTECTED_SCREENS} from '@src/SCREENS'; -import type {Screen} from '@src/SCREENS'; +import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; -import originalCloseRHPFlow from './closeRHPFlow'; -import originalDismissModal from './dismissModal'; -import originalDismissModalWithReport from './dismissModalWithReport'; -import getTopmostBottomTabRoute from './getTopmostBottomTabRoute'; -import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; -import originalGetTopmostReportActionId from './getTopmostReportActionID'; -import originalGetTopmostReportId from './getTopmostReportId'; -import isReportOpenInRHP from './isReportOpenInRHP'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import getInitialSplitNavigatorState from './AppNavigator/createSplitNavigator/getInitialSplitNavigatorState'; +import { + getMinimalAction, + getPolicyIDFromState, + getStateFromPath, + getTopmostReportParams, + isReportOpenInRHP, + isSplitNavigatorName, + linkTo, + closeRHPFlow as originalCloseRHPFlow, + setNavigationActionToMicrotaskQueue, +} from './helpers'; import linkingConfig from './linkingConfig'; -import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState'; -import linkTo from './linkTo'; +import RELATIONS from './linkingConfig/RELATIONS'; import navigationRef from './navigationRef'; -import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue'; -import switchPolicyID from './switchPolicyID'; -import type {NavigationStateRoute, RootStackParamList, State, StateOrRoute, SwitchPolicyIDParams} from './types'; +import type {NavigationPartialRoute, NavigationStateRoute, RootStackParamList, SplitNavigatorName, State} from './types'; let resolveNavigationIsReadyPromise: () => void; const navigationIsReadyPromise = new Promise((resolve) => { @@ -46,6 +52,21 @@ function setShouldPopAllStateOnUP(shouldPopAllStateFlag: boolean) { shouldPopAllStateOnUP = shouldPopAllStateFlag; } +/** + * @private + * Get the sidebar screen parameters from the split navigator passed as a param. + */ +function getSidebarScreenParams(splitNavigatorRoute: NavigationStateRoute) { + if (splitNavigatorRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + return splitNavigatorRoute.state?.routes?.at(0)?.params; + } + + return undefined; +} + +/** + * Checks if the navigationRef is ready to perform a method. + */ function canNavigate(methodName: string, params: Record = {}): boolean { if (navigationRef.isReady()) { return true; @@ -54,53 +75,23 @@ function canNavigate(methodName: string, params: Record = {}): return false; } -// Re-exporting the getTopmostReportId here to fill in default value for state. The getTopmostReportId isn't defined in this file to avoid cyclic dependencies. -const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopmostReportId(state); +/** + * Extracts from the topmost report its id. + */ +const getTopmostReportId = (state = navigationRef.getState()) => getTopmostReportParams(state)?.reportID; -// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies. -const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state); +/** + * Extracts from the topmost report its action id. + */ +const getTopmostReportActionId = (state = navigationRef.getState()) => getTopmostReportParams(state)?.reportActionID; -// Re-exporting the dismissModal here to fill in default value for navigationRef. The dismissModal isn't defined in this file to avoid cyclic dependencies. -const dismissModal = (reportID?: string, ref = navigationRef) => { - if (!reportID) { - originalDismissModal(ref); - return; - } - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - originalDismissModalWithReport({reportID, ...report}, ref); -}; -// Re-exporting the closeRHPFlow here to fill in default value for navigationRef. The closeRHPFlow isn't defined in this file to avoid cyclic dependencies. +/** + * Re-exporting the closeRHPFlow here to fill in default value for navigationRef. The closeRHPFlow isn't defined in this file to avoid cyclic dependencies. + */ const closeRHPFlow = (ref = navigationRef) => originalCloseRHPFlow(ref); -// Re-exporting the dismissModalWithReport here to fill in default value for navigationRef. The dismissModalWithReport isn't defined in this file to avoid cyclic dependencies. -// This method is needed because it allows to dismiss the modal and then open the report. Within this method is checked whether the report belongs to a specific workspace. Sometimes the report we want to check, hasn't been added to the Onyx yet. -// Then we can pass the report as a param without getting it from the Onyx. -const dismissModalWithReport = (report: OnyxEntry, ref = navigationRef) => originalDismissModalWithReport(report, ref); - -/** Method for finding on which index in stack we are. */ -function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined { - if ('routes' in stateOrRoute && stateOrRoute.routes) { - const childActiveRoute = stateOrRoute.routes[stateOrRoute.index ?? 0]; - return getActiveRouteIndex(childActiveRoute, stateOrRoute.index ?? 0); - } - - if ('state' in stateOrRoute && stateOrRoute.state?.routes) { - const childActiveRoute = stateOrRoute.state.routes[stateOrRoute.state.index ?? 0]; - return getActiveRouteIndex(childActiveRoute, stateOrRoute.state.index ?? 0); - } - - if ( - 'name' in stateOrRoute && - (stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || stateOrRoute.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR || stateOrRoute.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) - ) { - return 0; - } - - return index; -} - /** - * Function that generates dynamic urls from paths passed from OldDot + * Function that generates dynamic urls from paths passed from OldDot. */ function parseHybridAppUrl(url: HybridAppRoute | Route): Route { switch (url) { @@ -117,33 +108,8 @@ function parseHybridAppUrl(url: HybridAppRoute | Route): Route { } /** - * Gets distance from the path in root navigator. In other words how much screen you have to pop to get to the route with this path. - * The search is limited to 5 screens from the top for performance reasons. - * @param path - Path that you are looking for. - * @return - Returns distance to path or -1 if the path is not found in root navigator. + * Returns the current active route. */ -function getDistanceFromPathInRootNavigator(path?: string): number { - let currentState = navigationRef.getRootState(); - - for (let index = 0; index < 5; index++) { - if (!currentState.routes.length) { - break; - } - - // When comparing path and pathFromState, the policyID parameter isn't included in the comparison - const currentStateWithoutPolicyID = removePolicyIDParamFromState(currentState as State); - const pathFromState = getPathFromState(currentStateWithoutPolicyID, linkingConfig.config); - if (path === pathFromState.substring(1)) { - return index; - } - - currentState = {...currentState, routes: currentState.routes.slice(0, -1), index: currentState.index - 1}; - } - - return -1; -} - -/** Returns the current active route */ function getActiveRoute(): string { const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); if (!currentRoute?.name) { @@ -158,7 +124,9 @@ function getActiveRoute(): string { return ''; } - +/** + * Returns the route of a report opened in RHP. + */ function getReportRHPActiveRoute(): string { if (isReportOpenInRHP(navigationRef.getRootState())) { return getActiveRoute(); @@ -195,119 +163,191 @@ function navigate(route: Route = ROUTES.HOME, type?: string) { pendingRoute = route; return; } - linkTo(navigationRef.current, route, type, isActiveRoute(route)); + + linkTo(navigationRef.current, route, type); } /** - * @param fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP - * @param shouldEnforceFallback - Enforces navigation to fallback route - * @param shouldPopToTop - Should we navigate to LHN on back press + * 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. */ -function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) { - if (!canNavigate('goBack')) { - return; +const routeParamsIgnore = ['path', 'initial', 'params', 'state', 'screen', 'policyID']; + +/** + * @private + * If we use destructuring, we will get an error if any of the ignored properties are not present in the object. + */ +function getRouteParamsToCompare(routeParams: Record) { + return omit(routeParams, routeParamsIgnore); +} + +/** + * @private + * Private method used in goUp to determine whether a target route is present in the navigation state. + */ +function doesRouteMatchToMinimalActionPayload(route: NavigationStateRoute | NavigationPartialRoute, minimalAction: Writable, compareParams: boolean) { + if (!minimalAction.payload) { + return false; } - if (shouldPopToTop) { - if (shouldPopAllStateOnUP) { - shouldPopAllStateOnUP = false; - navigationRef.current?.dispatch(StackActions.popToTop()); - return; - } + if (!('name' in minimalAction.payload)) { + return false; } - if (!navigationRef.current?.canGoBack()) { - Log.hmmm('[Navigation] Unable to go back'); + const areRouteNamesEqual = route.name === minimalAction.payload.name; + + if (!areRouteNamesEqual) { + return false; + } + + if (!compareParams) { + return true; + } + + if (!('params' in minimalAction.payload)) { + return false; + } + + const routeParams = getRouteParamsToCompare(route.params as Record); + const minimalActionParams = getRouteParamsToCompare(minimalAction.payload.params as Record); + + return shallowCompare(routeParams, minimalActionParams); +} + +/** + * @private + * Checks whether the given state is the root navigator state + */ +function isRootNavigatorState(state: State): state is State { + return state.key === navigationRef.current?.getRootState().key; +} + +type GoBackOptions = { + /** + * If we should compare params when searching for a route in state to go up to. + * There are situations where we want to compare params when going up e.g. goUp to a specific report. + * Sometimes we want to go up and update params of screen e.g. country picker. + * In that case we want to goUp to a country picker with any params so we don't compare them. + */ + compareParams?: boolean; +}; + +const defaultGoBackOptions: Required = { + compareParams: true, +}; + +/** + * @private + * Navigate to the given fallbackRoute taking into account whether it is possible to go back to this screen. Within one nested navigator, we can go back by any number + * of screens, but if as a result of going back we would have to remove more than one screen from the rootState, + * replace is performed so as not to lose the visited pages. + * If fallbackRoute is not found in the state, replace is also called then. + * + * @param fallbackRoute - The route to go up. + * @param options - Optional configuration that affects navigation logic, such as parameter comparison. + */ +function goUp(fallbackRoute: Route, options?: GoBackOptions) { + if (!canNavigate('goUp')) { return; } - const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); - if (isFirstRouteInNavigator) { - const rootState = navigationRef.getRootState(); - const lastRoute = rootState.routes.at(-1); - // If the user comes from a different flow (there is more than one route in ModalNavigator) we should go back to the previous flow on UP button press instead of using the fallbackRoute. - if ((lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || lastRoute?.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR) && (lastRoute.state?.index ?? 0) > 0) { - navigationRef.current.goBack(); - return; - } + if (!navigationRef.current) { + Log.hmmm('[Navigation] Unable to go up'); + return; } - if (shouldEnforceFallback || (isFirstRouteInNavigator && fallbackRoute)) { - navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP); + const rootState = navigationRef.current.getRootState(); + const stateFromPath = getStateFromPath(fallbackRoute); + const action = getActionFromState(stateFromPath, linkingConfig.config); + + if (!action) { return; } - const isCentralPaneFocused = isCentralPaneName(findFocusedRoute(navigationRef.current.getState())?.name); - const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? ''); - - if (isCentralPaneFocused && fallbackRoute) { - // Allow CentralPane to use UP with fallback route if the path is not found in root navigator. - if (distanceFromPathInRootNavigator === -1) { - navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP); - return; - } - - // Add possibility to go back more than one screen in root navigator if that screen is on the stack. - if (distanceFromPathInRootNavigator > 0) { - navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator)); - return; - } + const {action: minimalAction, targetState} = getMinimalAction(action, rootState); + + if (minimalAction.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE || !targetState) { + return; } - // If the central pane is focused, it's possible that we navigated from other central pane with different matching bottom tab. - if (isCentralPaneFocused) { - const rootState = navigationRef.getRootState(); - const stateAfterPop = {routes: rootState.routes.slice(0, -1)} as State; - const topmostCentralPaneRouteAfterPop = getTopmostCentralPaneRoute(stateAfterPop); + const compareParams = options?.compareParams ?? defaultGoBackOptions.compareParams; + const indexOfFallbackRoute = targetState.routes.findLastIndex((route) => doesRouteMatchToMinimalActionPayload(route, minimalAction, compareParams)); - const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState as State); - const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateAfterPop); + const distanceToPop = targetState.routes.length - indexOfFallbackRoute - 1; - // If the central pane is defined after the pop action, we need to check if it's synced with the bottom tab screen. - // If not, we need to pop to the bottom tab screen/screens to sync it with the new central pane. - if (topmostCentralPaneRouteAfterPop && topmostBottomTabRoute?.name !== matchingBottomTabRoute.name) { - const bottomTabNavigator = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state; + // If we need to pop more than one route from rootState, we replace the current route to not lose visited routes from the navigation state + if (indexOfFallbackRoute === -1 || (isRootNavigatorState(targetState) && distanceToPop > 1)) { + const replaceAction = {...minimalAction, type: CONST.NAVIGATION.ACTION_TYPE.REPLACE} as NavigationAction; + navigationRef.current.dispatch(replaceAction); + return; + } - if (bottomTabNavigator && bottomTabNavigator.index) { - const matchingIndex = bottomTabNavigator.routes.findLastIndex((item) => item.name === matchingBottomTabRoute.name); - const indexToPop = matchingIndex !== -1 ? bottomTabNavigator.index - matchingIndex : undefined; - navigationRef.current.dispatch({...StackActions.pop(indexToPop), target: bottomTabNavigator?.key}); - } - } + /** + * If we are not comparing params, we want to use navigate action because it will replace params in the route already existing in the state if necessary. + * This part will need refactor after migrating to react-navigation 7. We will use popTo instead. + */ + if (!compareParams) { + navigationRef.current.dispatch(minimalAction); + return; } - navigationRef.current.goBack(); + navigationRef.current.dispatch({...StackActions.pop(distanceToPop), target: targetState.key}); } /** - * Close the current screen and navigate to the route. - * If the current screen is the first screen in the navigator, we force using the fallback route to replace the current screen. - * It's useful in a case where we want to close an RHP and navigate to another RHP to prevent any blink effect. + * @param fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP + * @param options - Optional configuration that affects navigation logic */ -function closeAndNavigate(route: Route) { - if (!navigationRef.current) { +function goBack(fallbackRoute?: Route, options?: GoBackOptions) { + if (!canNavigate('goBack')) { + return; + } + + if (fallbackRoute) { + goUp(fallbackRoute, options); + return; + } + + const rootState = navigationRef.current?.getRootState(); + const lastRoute = rootState?.routes.at(-1); + + const canGoBack = navigationRef.current?.canGoBack(); + + if (!canGoBack && isSplitNavigatorName(lastRoute?.name) && lastRoute?.state?.routes?.length === 1) { + const name = RELATIONS.SPLIT_TO_SIDEBAR[lastRoute.name as SplitNavigatorName]; + const params = getSidebarScreenParams(lastRoute); + navigationRef.dispatch({ + type: CONST.NAVIGATION.ACTION_TYPE.REPLACE, + payload: { + name, + params, + }, + }); return; } - const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); - if (isFirstRouteInNavigator) { - goBack(route, true); + if (!canGoBack) { + Log.hmmm('[Navigation] Unable to go back'); return; } - goBack(); - navigate(route); + + navigationRef.current?.goBack(); } /** - * Reset the navigation state to Home page + * Reset the navigation state to Home page. */ function resetToHome() { + const isNarrowLayout = getIsNarrowLayout(); const rootState = navigationRef.getRootState(); - const bottomTabKey = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state?.key; - if (bottomTabKey) { - navigationRef.dispatch({...StackActions.popToTop(), target: bottomTabKey}); - } navigationRef.dispatch({...StackActions.popToTop(), target: rootState.key}); + const splitNavigatorMainScreen = !isNarrowLayout + ? { + name: SCREENS.REPORT, + } + : undefined; + const payload = getInitialSplitNavigatorState({name: SCREENS.HOME}, splitNavigatorMainScreen); + navigationRef.dispatch({payload, type: 'REPLACE', target: rootState.key}); } /** @@ -321,13 +361,15 @@ function setParams(params: Record, routeKey = '') { } /** - * Returns the current active route without the URL params + * Returns the current active route without the URL params. */ function getActiveRouteWithoutParams(): string { return getActiveRoute().replace(/\?.*/, ''); } -/** Returns the active route name from a state event from the navigationRef */ +/** + * Returns the active route name from a state event from the navigationRef. + */ function getRouteNameFromStateEvent(event: EventArg<'state', false, NavigationContainerEventMap['state']['data']>): string | undefined { if (!event.data.state) { return; @@ -341,6 +383,7 @@ function getRouteNameFromStateEvent(event: EventArg<'state', false, NavigationCo } /** + * @private * Navigate to the route that we originally intended to go to * but the NavigationContainer was not ready when navigate() was called */ @@ -363,6 +406,7 @@ function setIsNavigationReady() { } /** + * @private * Checks if the navigation state contains routes that are protected (over the auth wall). * * @param state - react-navigation state object @@ -407,30 +451,74 @@ function waitForProtectedRoutes() { }); } -function navigateWithSwitchPolicyID(params: SwitchPolicyIDParams) { - if (!canNavigate('navigateWithSwitchPolicyID')) { +type NavigateToReportWithPolicyCheckPayload = {report?: OnyxEntry; reportID?: string; reportActionID?: string; referrer?: string; policyIDToCheck?: string}; + +/** + * Navigates to a report passed as a param (as an id or report object) and checks whether the target object belongs to the currently selected workspace. + * If not, the current workspace is set to global. + */ +function navigateToReportWithPolicyCheck({report, reportID, reportActionID, referrer, policyIDToCheck}: NavigateToReportWithPolicyCheckPayload, ref = navigationRef) { + const targetReport = reportID ? {reportID, ...ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]} : report; + const policyID = policyIDToCheck ?? getPolicyIDFromState(navigationRef.getRootState() as State); + const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID); + const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !ReportUtils.doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID); + + if ((shouldOpenAllWorkspace && !policyID) || !shouldOpenAllWorkspace) { + linkTo(ref.current, ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID ?? '-1', reportActionID, referrer)); return; } - return switchPolicyID(navigationRef.current, params); -} + const params: Record = { + reportID: targetReport?.reportID ?? '-1', + }; -function getTopMostCentralPaneRouteFromRootState() { - return getTopmostCentralPaneRoute(navigationRef.getRootState() as State); + if (reportActionID) { + params.reportActionID = reportActionID; + } + + if (referrer) { + params.referrer = referrer; + } + + ref.dispatch( + StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, { + policyID: null, + screen: SCREENS.REPORT, + params, + }), + ); } -function removeScreenFromNavigationState(screen: Screen) { - isNavigationReady().then(() => { - navigationRef.dispatch((state) => { - const routes = state.routes?.filter((item) => item.name !== screen); +/** + * Closes the modal navigator (RHP, LHP, onboarding). + */ +const dismissModal = (reportID?: string, ref = navigationRef) => { + ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + if (!reportID) { + return; + } + isNavigationReady().then(() => navigateToReportWithPolicyCheck({reportID})); +}; - return CommonActions.reset({ - ...state, - routes, - index: routes.length < state.routes.length ? state.index - 1 : state.index, - }); - }); - }); +/** + * Dismisses the modal and opens the given report. + */ +const dismissModalWithReport = (report: OnyxEntry) => { + dismissModal(); + isNavigationReady().then(() => navigateToReportWithPolicyCheck({report})); +}; + +/** + * Returns to the first screen in the stack, dismissing all the others, only if the global variable shouldPopAllStateOnUP is set to true. + */ +function popToTop() { + if (!shouldPopAllStateOnUP) { + goBack(); + return; + } + + shouldPopAllStateOnUP = false; + navigationRef.current?.dispatch(StackActions.popToTop()); } export default { @@ -443,7 +531,6 @@ export default { getActiveRoute, getActiveRouteWithoutParams, getReportRHPActiveRoute, - closeAndNavigate, goBack, isNavigationReady, setIsNavigationReady, @@ -452,12 +539,11 @@ export default { getTopmostReportActionId, waitForProtectedRoutes, parseHybridAppUrl, - navigateWithSwitchPolicyID, resetToHome, closeRHPFlow, setNavigationActionToMicrotaskQueue, - getTopMostCentralPaneRouteFromRootState, - removeScreenFromNavigationState, + navigateToReportWithPolicyCheck, + popToTop, }; export {navigationRef}; diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index cff9fd49fb7a..a8af25db8e62 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -4,8 +4,8 @@ import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentReportID from '@hooks/useCurrentReportID'; +import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemePreference from '@hooks/useThemePreference'; @@ -22,13 +22,10 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import AppNavigator from './AppNavigator'; -import getPolicyIDFromState from './getPolicyIDFromState'; +import {cleanPreservedSplitNavigatorStates} from './AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState'; +import {customGetPathFromState, getAdaptedStateFromPath, setupCustomAndroidBackHandler} from './helpers'; import linkingConfig from './linkingConfig'; -import customGetPathFromState from './linkingConfig/customGetPathFromState'; -import getAdaptedStateFromPath from './linkingConfig/getAdaptedStateFromPath'; import Navigation, {navigationRef} from './Navigation'; -import setupCustomAndroidBackHandler from './setupCustomAndroidBackHandler'; -import type {RootStackParamList} from './types'; type NavigationRootProps = { /** Whether the current user is logged in with an authToken */ @@ -90,13 +87,14 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh const currentReportIDValue = useCurrentReportID(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {setActiveWorkspaceID} = useActiveWorkspace(); const [user] = useOnyx(ONYXKEYS.USER); const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasCompletedGuidedSetupFlowSelector, }); + const previousAuthenticated = usePrevious(authenticated); + const initialState = useMemo(() => { if (!user || user.isFromPublicDomain) { return; @@ -158,6 +156,22 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh Navigation.setShouldPopAllStateOnUP(!shouldUseNarrowLayout); }, [shouldUseNarrowLayout]); + useEffect(() => { + // Since the NAVIGATORS.REPORTS_SPLIT_NAVIGATOR url is "/" and it has to be used as an URL for SignInPage, + // this navigator should be the only one in the navigation state after logout. + const hasUserLoggedOut = !authenticated && !!previousAuthenticated; + if (!hasUserLoggedOut) { + return; + } + + const rootState = navigationRef.getRootState(); + const lastRoute = rootState.routes.at(-1); + if (!lastRoute) { + return; + } + navigationRef.reset({...rootState, index: 0, routes: [{...lastRoute, params: {}}]}); + }, [authenticated, previousAuthenticated]); + const handleStateChange = (state: NavigationState | undefined) => { if (!state) { return; @@ -165,16 +179,15 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh const currentRoute = navigationRef.getCurrentRoute(); Firebase.log(`[NAVIGATION] screen: ${currentRoute?.name}, params: ${JSON.stringify(currentRoute?.params ?? {})}`); - const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render setTimeout(() => { currentReportIDValue?.updateCurrentReportID(state); - setActiveWorkspaceID(activeWorkspaceID); }, 0); parseAndLogRoute(state); // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); + cleanPreservedSplitNavigatorStates(state); }; return ( diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.native.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.native.tsx index 35076c8ca6b6..1f3b4a4c04ce 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.native.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.native.tsx @@ -20,12 +20,22 @@ function createPlatformStackNavigatorComponent ({stateToRender: undefined, searchRoute: undefined})); + const useCustomState = options?.useCustomState ?? (() => undefined); const useCustomEffects = options?.useCustomEffects ?? (() => undefined); const ExtraContent = options?.ExtraContent; const NavigationContentWrapper = options?.NavigationContentWrapper; - function PlatformNavigator({id, initialRouteName, screenOptions, screenListeners, children, ...props}: PlatformStackNavigatorProps) { + function PlatformNavigator({ + id, + initialRouteName, + screenOptions, + screenListeners, + children, + sidebarScreen, + defaultCentralScreen, + parentRoute, + ...props + }: PlatformStackNavigatorProps) { const { navigation, state: originalState, @@ -47,6 +57,9 @@ function createPlatformStackNavigatorComponent, convertToNativeNavigationOptions, ); @@ -57,19 +70,19 @@ function createPlatformStackNavigatorComponent stateToRender ?? originalState, [originalState, stateToRender]); const customCodePropsWithCustomState = useMemo>>( () => ({ ...customCodeProps, state, - searchRoute, }), - [customCodeProps, state, searchRoute], + [customCodeProps, state], ); // Executes custom effects defined in "useCustomEffects" navigator option. diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index 2e3c99a6cb0d..83af4cc9bd95 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -19,13 +19,23 @@ function createPlatformStackNavigatorComponent, ) { const createRouter = options?.createRouter ?? StackRouter; - const useCustomState = options?.useCustomState ?? (() => ({stateToRender: undefined, searchRoute: undefined})); + const useCustomState = options?.useCustomState ?? (() => undefined); const defaultScreenOptions = options?.defaultScreenOptions; const ExtraContent = options?.ExtraContent; const NavigationContentWrapper = options?.NavigationContentWrapper; const useCustomEffects = options?.useCustomEffects ?? (() => undefined); - function PlatformNavigator({id, initialRouteName, screenOptions, screenListeners, children, ...props}: PlatformStackNavigatorProps) { + function PlatformNavigator({ + id, + initialRouteName, + screenOptions, + screenListeners, + children, + sidebarScreen, + defaultCentralScreen, + parentRoute, + ...props + }: PlatformStackNavigatorProps) { const { navigation, state: originalState, @@ -47,6 +57,9 @@ function createPlatformStackNavigatorComponent, convertToWebNavigationOptions, ); @@ -57,21 +70,20 @@ function createPlatformStackNavigatorComponent stateToRender ?? originalState, [originalState, stateToRender]); const customCodePropsWithCustomState = useMemo>>( () => ({ ...customCodeProps, state, - searchRoute, }), - [customCodeProps, state, searchRoute], + [customCodeProps, state], ); - // Executes custom effects defined in "useCustomEffects" navigator option. useCustomEffects(customCodePropsWithCustomState); diff --git a/src/libs/Navigation/PlatformStackNavigation/types/NavigationBuilder.ts b/src/libs/Navigation/PlatformStackNavigation/types/NavigationBuilder.ts index 821584f58645..e3199b27997b 100644 --- a/src/libs/Navigation/PlatformStackNavigation/types/NavigationBuilder.ts +++ b/src/libs/Navigation/PlatformStackNavigation/types/NavigationBuilder.ts @@ -20,7 +20,13 @@ type PlatformNavigationBuilderOptions< EventMap extends PlatformSpecificEventMap & EventMapBase, ParamList extends ParamListBase = ParamListBase, RouterOptions extends PlatformStackRouterOptions = PlatformStackRouterOptions, -> = DefaultNavigatorOptions, NavigationOptions, EventMap> & NavigationBuilderOptions & RouterOptions; +> = DefaultNavigatorOptions, NavigationOptions, EventMap> & + NavigationBuilderOptions & + RouterOptions & { + defaultCentralScreen?: Extract; + sidebarScreen?: Extract; + parentRoute?: RouteProp; + }; // Represents the return type of the useNavigationBuilder function using the types from PlatformStackNavigation. type PlatformNavigationBuilderResult< diff --git a/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts b/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts index 5a0dd8602bc0..2f170b202181 100644 --- a/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts +++ b/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts @@ -1,4 +1,4 @@ -import type {EventMapBase, ParamListBase, StackActionHelpers} from '@react-navigation/native'; +import type {EventMapBase, ParamListBase, RouteProp, StackActionHelpers} from '@react-navigation/native'; import type { PlatformSpecificEventMap, PlatformSpecificNavigationOptions, @@ -9,9 +9,6 @@ import type { } from '.'; import type {PlatformNavigationBuilderDescriptors, PlatformNavigationBuilderNavigation} from './NavigationBuilder'; -// Represents a route in the search context within the navigation state. -type SearchRoute = PlatformStackNavigationState['routes'][number]; - // Props that custom code receives when passed to the createPlatformStackNavigatorComponent generator function. // Custom logic like "transformState", "onWindowDimensionsChange" and custom components like "NavigationContentWrapper" and "ExtraContent" will receive these props type CustomCodeProps< @@ -24,17 +21,14 @@ type CustomCodeProps< navigation: PlatformNavigationBuilderNavigation; descriptors: PlatformNavigationBuilderDescriptors; displayName: string; - searchRoute?: SearchRoute; + parentRoute?: RouteProp; }; // Props for the custom state hook. type CustomStateHookProps = CustomCodeProps; -// Defines a hook function type for transforming the navigation state based on props, and returning the transformed state and search route. -type CustomStateHook = (props: CustomStateHookProps) => { - stateToRender?: PlatformStackNavigationState; - searchRoute?: SearchRoute; -}; +// Defines a hook function type for transforming the navigation state based on props, and returning the transformed state. +type CustomStateHook = (props: CustomStateHookProps) => PlatformStackNavigationState; // Props for the custom effects hook. type CustomEffectsHookProps = CustomCodeProps; diff --git a/src/libs/Navigation/PlatformStackNavigation/types/index.ts b/src/libs/Navigation/PlatformStackNavigation/types/index.ts index 04ed4e68d9a8..ff7515e300d4 100644 --- a/src/libs/Navigation/PlatformStackNavigation/types/index.ts +++ b/src/libs/Navigation/PlatformStackNavigation/types/index.ts @@ -27,7 +27,7 @@ type PlatformStackNavigationEventMap = CommonStackNavigationEventMap; type PlatformSpecificEventMap = StackNavigationOptions | NativeStackNavigationOptions; // Router options used in the PlatformStackNavigation -type PlatformStackRouterOptions = StackRouterOptions; +type PlatformStackRouterOptions = StackRouterOptions & {parentRoute?: RouteProp}; // Factory function type for creating a router specific to the PlatformStackNavigation type PlatformStackRouterFactory = RouterFactory< @@ -68,7 +68,10 @@ type PlatformStackNavigatorProps< RouterOptions extends PlatformStackRouterOptions = PlatformStackRouterOptions, > = DefaultNavigatorOptions, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, RouteName> & RouterOptions & - StackNavigationConfig; + StackNavigationConfig & { + defaultCentralScreen?: Extract; + sidebarScreen?: Extract; + }; // The "screenOptions" and "defaultScreenOptions" can either be an object of navigation options or // a factory function that returns the navigation options based on route and navigation props. diff --git a/src/libs/Navigation/dismissModal.ts b/src/libs/Navigation/dismissModal.ts deleted file mode 100644 index dd0e512ea33d..000000000000 --- a/src/libs/Navigation/dismissModal.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type {NavigationContainerRef} from '@react-navigation/native'; -import {StackActions} from '@react-navigation/native'; -import Log from '@libs/Log'; -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; -import type {RootStackParamList} from './types'; - -// This function is in a separate file than Navigation.ts to avoid cyclic dependency. - -/** - * Dismisses the last modal stack if there is any - */ -function dismissModal(navigationRef: NavigationContainerRef) { - if (!navigationRef.isReady()) { - return; - } - - const state = navigationRef.getState(); - const lastRoute = state.routes.at(-1); - switch (lastRoute?.name) { - case NAVIGATORS.FULL_SCREEN_NAVIGATOR: - case NAVIGATORS.LEFT_MODAL_NAVIGATOR: - case NAVIGATORS.RIGHT_MODAL_NAVIGATOR: - case NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR: - case NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR: - case SCREENS.NOT_FOUND: - case SCREENS.ATTACHMENTS: - case SCREENS.TRANSACTION_RECEIPT: - case SCREENS.PROFILE_AVATAR: - case SCREENS.WORKSPACE_AVATAR: - case SCREENS.REPORT_AVATAR: - case SCREENS.CONCIERGE: - navigationRef.dispatch({...StackActions.pop(), target: state.key}); - break; - default: { - Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss'); - } - } -} - -export default dismissModal; diff --git a/src/libs/Navigation/dismissModalWithReport.ts b/src/libs/Navigation/dismissModalWithReport.ts deleted file mode 100644 index 854b2e586caf..000000000000 --- a/src/libs/Navigation/dismissModalWithReport.ts +++ /dev/null @@ -1,80 +0,0 @@ -import {getActionFromState} from '@react-navigation/core'; -import type {NavigationContainerRef} from '@react-navigation/native'; -import {StackActions} from '@react-navigation/native'; -import findLastIndex from 'lodash/findLastIndex'; -import type {OnyxEntry} from 'react-native-onyx'; -import Log from '@libs/Log'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; -import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; -import NAVIGATORS from '@src/NAVIGATORS'; -import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; -import type {Report} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import getPolicyIDFromState from './getPolicyIDFromState'; -import getStateFromPath from './getStateFromPath'; -import getTopmostReportId from './getTopmostReportId'; -import linkingConfig from './linkingConfig'; -import switchPolicyID from './switchPolicyID'; -import type {RootStackParamList, StackNavigationAction, State} from './types'; - -// This function is in a separate file than Navigation.ts to avoid cyclic dependency. - -/** - * Dismisses the last modal stack if there is any - * - * @param targetReportID - The reportID to navigate to after dismissing the modal - */ -function dismissModalWithReport(targetReport: OnyxEntry, navigationRef: NavigationContainerRef) { - if (!navigationRef.isReady()) { - return; - } - - const state = navigationRef.getState(); - const lastRoute = state.routes.at(-1); - switch (lastRoute?.name) { - case NAVIGATORS.FULL_SCREEN_NAVIGATOR: - case NAVIGATORS.LEFT_MODAL_NAVIGATOR: - case NAVIGATORS.RIGHT_MODAL_NAVIGATOR: - case SCREENS.NOT_FOUND: - case SCREENS.ATTACHMENTS: - case SCREENS.TRANSACTION_RECEIPT: - case SCREENS.PROFILE_AVATAR: - case SCREENS.WORKSPACE_AVATAR: - case SCREENS.REPORT_AVATAR: - case SCREENS.CONCIERGE: - // If we are not in the target report, we need to navigate to it after dismissing the modal - if (targetReport?.reportID !== getTopmostReportId(state)) { - const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID ?? '-1')); - const policyID = getPolicyIDFromState(state as State); - const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID); - const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID); - - if (shouldOpenAllWorkspace) { - switchPolicyID(navigationRef, {route: ROUTES.HOME}); - } else { - switchPolicyID(navigationRef, {policyID, route: ROUTES.HOME}); - } - - const action: StackNavigationAction = getActionFromState(reportState, linkingConfig.config); - if (action) { - action.type = 'REPLACE'; - navigationRef.dispatch(action); - } - // If not-found page is in the route stack, we need to close it - } else if (state.routes.some((route) => route.name === SCREENS.NOT_FOUND)) { - const lastRouteIndex = state.routes.length - 1; - const centralRouteIndex = findLastIndex(state.routes, (route) => isCentralPaneName(route.name)); - navigationRef.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: state.key}); - } else { - navigationRef.dispatch({...StackActions.pop(), target: state.key}); - } - break; - default: { - Log.hmmm('[Navigation] dismissModalWithReport failed because there is no modal stack to dismiss'); - } - } -} - -export default dismissModalWithReport; diff --git a/src/libs/Navigation/getPolicyIDFromState.ts b/src/libs/Navigation/getPolicyIDFromState.ts deleted file mode 100644 index 702fb654780d..000000000000 --- a/src/libs/Navigation/getPolicyIDFromState.ts +++ /dev/null @@ -1,30 +0,0 @@ -import SCREENS from '@src/SCREENS'; -import extractPolicyIDFromQuery from './extractPolicyIDFromQuery'; -import getTopmostBottomTabRoute from './getTopmostBottomTabRoute'; -import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; -import type {RootStackParamList, State} from './types'; - -/** - * returns policyID value if one exists in navigation state - * - * PolicyID in this app can be stored in two ways: - * - on most screens but NOT Search as `policyID` param (on bottom tab screens) - * - on Search related screens as policyID filter inside `q` (SearchQuery) param (only for SEARCH_CENTRAL_PANE) - */ -const getPolicyIDFromState = (state: State): string | undefined => { - const topmostBottomTabRoute = getTopmostBottomTabRoute(state); - - if (!topmostBottomTabRoute) { - return; - } - - if (topmostBottomTabRoute.name === SCREENS.SEARCH.BOTTOM_TAB) { - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state); - return extractPolicyIDFromQuery(topmostCentralPaneRoute); - } - - const policyID = topmostBottomTabRoute && topmostBottomTabRoute.params && 'policyID' in topmostBottomTabRoute.params && topmostBottomTabRoute.params?.policyID; - return policyID ? (topmostBottomTabRoute.params?.policyID as string) : undefined; -}; - -export default getPolicyIDFromState; diff --git a/src/libs/Navigation/getTopmostBottomTabRoute.ts b/src/libs/Navigation/getTopmostBottomTabRoute.ts deleted file mode 100644 index 48a8d80f4096..000000000000 --- a/src/libs/Navigation/getTopmostBottomTabRoute.ts +++ /dev/null @@ -1,21 +0,0 @@ -import NAVIGATORS from '@src/NAVIGATORS'; -import type {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from './types'; - -function getTopmostBottomTabRoute(state: State | undefined): NavigationPartialRoute | undefined { - const bottomTabNavigatorRoute = state?.routes.findLast((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); - - // The bottomTabNavigatorRoute state may be empty if we just logged in. - if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR || bottomTabNavigatorRoute.state === undefined) { - return undefined; - } - - const topmostBottomTabRoute = bottomTabNavigatorRoute.state.routes.at(-1); - - if (!topmostBottomTabRoute) { - throw new Error('BottomTabNavigator route have no routes.'); - } - - return {name: topmostBottomTabRoute.name as BottomTabName, params: topmostBottomTabRoute.params}; -} - -export default getTopmostBottomTabRoute; diff --git a/src/libs/Navigation/getTopmostCentralPaneRoute.ts b/src/libs/Navigation/getTopmostCentralPaneRoute.ts deleted file mode 100644 index 5ac72281eaf6..000000000000 --- a/src/libs/Navigation/getTopmostCentralPaneRoute.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {isCentralPaneName} from '@libs/NavigationUtils'; -import type {CentralPaneName, NavigationPartialRoute, RootStackParamList, State} from './types'; - -// Get the name of topmost central pane route in the navigation stack. -function getTopmostCentralPaneRoute(state: State): NavigationPartialRoute | undefined { - if (!state) { - return; - } - - const topmostCentralPane = state.routes.filter((route) => isCentralPaneName(route.name)).at(-1); - - if (!topmostCentralPane) { - return; - } - - return topmostCentralPane as NavigationPartialRoute; -} - -export default getTopmostCentralPaneRoute; diff --git a/src/libs/Navigation/getTopmostFullScreenRoute.ts b/src/libs/Navigation/getTopmostFullScreenRoute.ts deleted file mode 100644 index fcc28ce76926..000000000000 --- a/src/libs/Navigation/getTopmostFullScreenRoute.ts +++ /dev/null @@ -1,28 +0,0 @@ -import NAVIGATORS from '@src/NAVIGATORS'; -import type {FullScreenName, NavigationPartialRoute, RootStackParamList, State} from './types'; - -// Get the name of topmost fullscreen route in the navigation stack. -function getTopmostFullScreenRoute(state: State): NavigationPartialRoute | undefined { - if (!state) { - return; - } - - const topmostFullScreenRoute = state.routes.filter((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR).at(-1); - - if (!topmostFullScreenRoute) { - return; - } - - if (topmostFullScreenRoute.state) { - // There will be at least one route in the fullscreen navigator. - const {name, params} = topmostFullScreenRoute.state.routes.at(-1) as NavigationPartialRoute; - - return {name, params}; - } - - if (!!topmostFullScreenRoute.params && 'screen' in topmostFullScreenRoute.params) { - return {name: topmostFullScreenRoute.params.screen as FullScreenName, params: topmostFullScreenRoute.params.params}; - } -} - -export default getTopmostFullScreenRoute; diff --git a/src/libs/Navigation/getTopmostReportActionID.ts b/src/libs/Navigation/getTopmostReportActionID.ts deleted file mode 100644 index d3c6e41887d8..000000000000 --- a/src/libs/Navigation/getTopmostReportActionID.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type {NavigationState, PartialState} from '@react-navigation/native'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import SCREENS from '@src/SCREENS'; -import type {RootStackParamList} from './types'; - -// This function is in a separate file than Navigation.ts to avoid cyclic dependency. - -/** - * Find the last visited report screen in the navigation state and get the linked reportActionID of it. - * - * @param state - The react-navigation state - * @returns - It's possible that there is no report screen - */ -function getTopmostReportActionID(state: NavigationState | NavigationState | PartialState): string | undefined { - if (!state) { - return; - } - - const topmostCentralPane = state.routes.filter((route) => isCentralPaneName(route.name)).at(-1); - if (!topmostCentralPane) { - return; - } - - const directReportParams = topmostCentralPane.params; - const directReportActionIDParam = directReportParams && 'reportActionID' in directReportParams && directReportParams?.reportActionID; - - if (!topmostCentralPane.state && !directReportActionIDParam) { - return; - } - - if (directReportActionIDParam) { - return directReportActionIDParam; - } - - const topmostReport = topmostCentralPane.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1); - if (!topmostReport) { - return; - } - - const topmostReportActionID = topmostReport.params && 'reportActionID' in topmostReport.params && topmostReport.params?.reportActionID; - if (typeof topmostReportActionID !== 'string') { - return; - } - - return topmostReportActionID; -} - -export default getTopmostReportActionID; diff --git a/src/libs/Navigation/getTopmostReportId.ts b/src/libs/Navigation/getTopmostReportId.ts deleted file mode 100644 index dc53d040f087..000000000000 --- a/src/libs/Navigation/getTopmostReportId.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type {NavigationState, PartialState} from '@react-navigation/native'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import SCREENS from '@src/SCREENS'; -import type {RootStackParamList} from './types'; - -// This function is in a separate file than Navigation.ts to avoid cyclic dependency. - -/** - * Find the last visited report screen in the navigation state and get the id of it. - * - * @param state - The react-navigation state - * @returns - It's possible that there is no report screen - */ -function getTopmostReportId(state: NavigationState | NavigationState | PartialState): string | undefined { - if (!state) { - return; - } - - const topmostCentralPane = state.routes?.filter((route) => isCentralPaneName(route.name)).at(-1); - if (!topmostCentralPane) { - return; - } - - const directReportParams = topmostCentralPane.params; - const directReportIdParam = directReportParams && 'reportID' in directReportParams && directReportParams?.reportID; - - if (!topmostCentralPane.state && !directReportIdParam) { - return; - } - - if (directReportIdParam) { - return directReportIdParam; - } - - const topmostReport = topmostCentralPane.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1); - if (!topmostReport) { - return; - } - - const topmostReportId = topmostReport.params && 'reportID' in topmostReport.params && topmostReport.params?.reportID; - if (typeof topmostReportId !== 'string') { - return; - } - - return topmostReportId; -} - -export default getTopmostReportId; diff --git a/src/libs/Navigation/closeRHPFlow.ts b/src/libs/Navigation/helpers/closeRHPFlow.ts similarity index 94% rename from src/libs/Navigation/closeRHPFlow.ts rename to src/libs/Navigation/helpers/closeRHPFlow.ts index 9bc40f51f472..0f814ca13bb7 100644 --- a/src/libs/Navigation/closeRHPFlow.ts +++ b/src/libs/Navigation/helpers/closeRHPFlow.ts @@ -1,8 +1,8 @@ import type {NavigationContainerRef} from '@react-navigation/native'; import {StackActions} from '@react-navigation/native'; import Log from '@libs/Log'; +import type {RootStackParamList} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; -import type {RootStackParamList} from './types'; /** * Closes the last RHP flow, if there is only one, closes the entire RHP. diff --git a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts b/src/libs/Navigation/helpers/createNormalizedConfigs.ts similarity index 100% rename from src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts rename to src/libs/Navigation/helpers/createNormalizedConfigs.ts diff --git a/src/libs/Navigation/helpers/customGetPathFromState.ts b/src/libs/Navigation/helpers/customGetPathFromState.ts new file mode 100644 index 000000000000..24fa3dbe1321 --- /dev/null +++ b/src/libs/Navigation/helpers/customGetPathFromState.ts @@ -0,0 +1,21 @@ +import {getPathFromState} from '@react-navigation/native'; +import NAVIGATORS from '@src/NAVIGATORS'; +import {isFullScreenName} from './isNavigatorName'; + +// This function adds the policyID param to the url. +const customGetPathFromState: typeof getPathFromState = (state, options) => { + const path = getPathFromState(state, options); + const fullScreenRoute = state.routes.findLast((route) => isFullScreenName(route.name)); + + const shouldAddPolicyID = fullScreenRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; + + if (!shouldAddPolicyID) { + return path; + } + + const policyID = fullScreenRoute.params && `policyID` in fullScreenRoute.params ? (fullScreenRoute.params.policyID as string) : undefined; + + return `${policyID ? `/w/${policyID}` : ''}${path}`; +}; + +export default customGetPathFromState; diff --git a/src/libs/Navigation/extractPolicyIDFromQuery.ts b/src/libs/Navigation/helpers/extractPolicyIDFromQuery.ts similarity index 89% rename from src/libs/Navigation/extractPolicyIDFromQuery.ts rename to src/libs/Navigation/helpers/extractPolicyIDFromQuery.ts index f091690c16f2..d37ded16b4b5 100644 --- a/src/libs/Navigation/extractPolicyIDFromQuery.ts +++ b/src/libs/Navigation/helpers/extractPolicyIDFromQuery.ts @@ -1,5 +1,5 @@ +import type {NavigationPartialRoute} from '@libs/Navigation/types'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; -import type {NavigationPartialRoute} from './types'; function extractPolicyIDFromQuery(route?: NavigationPartialRoute) { if (!route?.params) { diff --git a/src/libs/Navigation/extrapolateStateFromParams.ts b/src/libs/Navigation/helpers/extrapolateStateFromParams.ts similarity index 100% rename from src/libs/Navigation/extrapolateStateFromParams.ts rename to src/libs/Navigation/helpers/extrapolateStateFromParams.ts diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts new file mode 100644 index 000000000000..8a2886c36f27 --- /dev/null +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -0,0 +1,242 @@ +import type {NavigationState, PartialState, Route} from '@react-navigation/native'; +import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; +import pick from 'lodash/pick'; +import {isAnonymousUser} from '@libs/actions/Session'; +import getInitialSplitNavigatorState from '@libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState'; +import config from '@libs/Navigation/linkingConfig/config'; +import RELATIONS from '@libs/Navigation/linkingConfig/RELATIONS'; +import type {NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; +import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; +import * as ReportConnection from '@libs/ReportConnection'; +import NAVIGATORS from '@src/NAVIGATORS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import extractPolicyIDFromQuery from './extractPolicyIDFromQuery'; +import getParamsFromRoute from './getParamsFromRoute'; +import {isFullScreenName} from './isNavigatorName'; +import replacePathInNestedState from './replacePathInNestedState'; + +type GetAdaptedStateReturnType = { + adaptedState: ReturnType; +}; + +type GetAdaptedStateFromPath = (...args: [...Parameters, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType; + +// The function getPathFromState that we are using in some places isn't working correctly without defined index. +const getRoutesWithIndex = (routes: NavigationPartialRoute[]): PartialState => ({routes, index: routes.length - 1}); + +function isRouteWithBackToParam(route: NavigationPartialRoute): route is Route { + return route.params !== undefined && 'backTo' in route.params && typeof route.params.backTo === 'string'; +} + +function isRouteWithReportID(route: NavigationPartialRoute): route is Route { + return route.params !== undefined && 'reportID' in route.params && typeof route.params.reportID === 'string'; +} + +function getMatchingFullScreenRoute(route: NavigationPartialRoute, policyID?: string) { + // Check for backTo param. One screen with different backTo value may need different screens visible under the overlay. + if (isRouteWithBackToParam(route)) { + const stateForBackTo = getStateFromPath(route.params.backTo, config); + + // This may happen if the backTo url is invalid. + const lastRoute = stateForBackTo?.routes.at(-1); + if (!stateForBackTo || !lastRoute || lastRoute.name === SCREENS.NOT_FOUND) { + return undefined; + } + + const isLastRouteFullScreen = isFullScreenName(lastRoute.name); + + // If the state for back to last route is a full screen route, we can use it + if (isLastRouteFullScreen) { + return lastRoute; + } + + const focusedStateForBackToRoute = findFocusedRoute(stateForBackTo); + + if (!focusedStateForBackToRoute) { + return undefined; + } + // If not, get the matching full screen route for the back to state. + return getMatchingFullScreenRoute(focusedStateForBackToRoute, policyID); + } + + if (RELATIONS.SEARCH_TO_RHP.includes(route.name)) { + const paramsFromRoute = getParamsFromRoute(SCREENS.SEARCH.CENTRAL_PANE); + + return { + name: SCREENS.SEARCH.CENTRAL_PANE, + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }; + } + + if (RELATIONS.RHP_TO_SIDEBAR[route.name]) { + return getInitialSplitNavigatorState( + { + name: RELATIONS.RHP_TO_SIDEBAR[route.name], + }, + undefined, + policyID ? {policyID} : undefined, + ); + } + + if (RELATIONS.RHP_TO_WORKSPACE[route.name]) { + const paramsFromRoute = getParamsFromRoute(RELATIONS.RHP_TO_WORKSPACE[route.name]); + + return getInitialSplitNavigatorState( + { + name: SCREENS.WORKSPACE.INITIAL, + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }, + { + name: RELATIONS.RHP_TO_WORKSPACE[route.name], + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }, + ); + } + + if (RELATIONS.RHP_TO_SETTINGS[route.name]) { + const paramsFromRoute = getParamsFromRoute(RELATIONS.RHP_TO_SETTINGS[route.name]); + + return getInitialSplitNavigatorState( + { + name: SCREENS.SETTINGS.ROOT, + }, + { + name: RELATIONS.RHP_TO_SETTINGS[route.name], + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }, + ); + } + + return undefined; +} + +// If there is no particular matching route defined, we want to get the default route. +// It is the reports split navigator with report. If the reportID is defined in the focused route, we want to use it for the default report. +// This is separated from getMatchingFullScreenRoute because we want to use it only for the initial state. +// We don't want to make this route mandatory e.g. after deep linking or opening a specific flow. +function getDefaultFullScreenRoute(route?: NavigationPartialRoute, policyID?: string) { + // We will use it if the reportID is not defined. Router of this navigator has logic to fill it with a report. + const fallbackRoute = { + name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, + }; + + if (route && isRouteWithReportID(route)) { + const reportID = route.params.reportID; + + if (!ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) { + return fallbackRoute; + } + + return getInitialSplitNavigatorState( + { + name: SCREENS.HOME, + }, + { + name: SCREENS.REPORT, + params: {reportID}, + }, + policyID ? {policyID} : undefined, + ); + } + + return fallbackRoute; +} + +function getOnboardingAdaptedState(state: PartialState): PartialState { + const onboardingRoute = state.routes.at(0); + if (!onboardingRoute || onboardingRoute.name === SCREENS.ONBOARDING.PURPOSE) { + return state; + } + + const routes = []; + routes.push({name: SCREENS.ONBOARDING.PURPOSE}); + if (onboardingRoute.name === SCREENS.ONBOARDING.ACCOUNTING) { + routes.push({name: SCREENS.ONBOARDING.EMPLOYEES}); + } + routes.push(onboardingRoute); + + return getRoutesWithIndex(routes); +} + +function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType { + const fullScreenRoute = state.routes.find((route) => isFullScreenName(route.name)); + const reportsSplitNavigator = state.routes.find((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); + const onboardingNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR); + + // If policyID is defined, it should be passed to the reportNavigator params. + if (reportsSplitNavigator && policyID) { + const routes = []; + const reportNavigatorWithPolicyID = {...reportsSplitNavigator}; + reportNavigatorWithPolicyID.params = {...reportNavigatorWithPolicyID.params, policyID}; + routes.push(reportNavigatorWithPolicyID); + + return { + adaptedState: getRoutesWithIndex(routes), + }; + } + + // If there is no full screen route in the root, we want to add it. + if (!fullScreenRoute) { + const focusedRoute = findFocusedRoute(state); + + if (focusedRoute) { + const matchingRootRoute = getMatchingFullScreenRoute(focusedRoute, policyID); + // If there is a matching root route, add it to the state. + if (matchingRootRoute) { + return { + adaptedState: getRoutesWithIndex([matchingRootRoute, ...state.routes]), + }; + } + } + + const defaultFullScreenRoute = getDefaultFullScreenRoute(focusedRoute, policyID); + + // The onboarding flow consists of several screens. If we open any of the screens, the previous screens from that flow should be in the state. + if (onboardingNavigator?.state) { + const adaptedOnboardingNavigator = { + ...onboardingNavigator, + state: getOnboardingAdaptedState(onboardingNavigator.state), + }; + + return { + adaptedState: getRoutesWithIndex([defaultFullScreenRoute, adaptedOnboardingNavigator]), + }; + } + + // If not, add the default full screen route. + return { + adaptedState: getRoutesWithIndex([defaultFullScreenRoute, ...state.routes]), + }; + } + + return { + adaptedState: state, + }; +} + +const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldReplacePathInNestedState = true) => { + const normalizedPath = !path.startsWith('/') ? `/${path}` : path; + const pathWithoutPolicyID = getPathWithoutPolicyID(normalizedPath); + const isAnonymous = isAnonymousUser(); + + // Anonymous users don't have access to workspaces + const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path); + + const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; + if (shouldReplacePathInNestedState) { + replacePathInNestedState(state, normalizedPath); + } + + if (state === undefined) { + throw new Error('Unable to parse path'); + } + + // On SCREENS.SEARCH.CENTRAL_PANE policyID is stored differently inside search query ("q" param), so we're handling this case + const focusedRoute = findFocusedRoute(state); + const policyIDFromQuery = extractPolicyIDFromQuery(focusedRoute); + return getAdaptedState(state, policyID ?? policyIDFromQuery); +}; + +export default getAdaptedStateFromPath; +export {getMatchingFullScreenRoute, isFullScreenName}; diff --git a/src/libs/Navigation/linkingConfig/getOnboardingAdaptedState.ts b/src/libs/Navigation/helpers/getOnboardingAdaptedState.ts similarity index 76% rename from src/libs/Navigation/linkingConfig/getOnboardingAdaptedState.ts rename to src/libs/Navigation/helpers/getOnboardingAdaptedState.ts index eee3f9f5e52d..97f02bd91509 100644 --- a/src/libs/Navigation/linkingConfig/getOnboardingAdaptedState.ts +++ b/src/libs/Navigation/helpers/getOnboardingAdaptedState.ts @@ -1,6 +1,10 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import SCREENS from '@src/SCREENS'; +/** + * When we open the application via deeplink to a specific onboarding screen, we want the previous onboarding screens to be able to go back to them. + * Therefore, the routes of the previous screens are added here. + */ export default function getOnboardingAdaptedState(state: PartialState): PartialState { const onboardingRoute = state.routes.at(0); if (!onboardingRoute || onboardingRoute.name === SCREENS.ONBOARDING.PURPOSE) { diff --git a/src/libs/Navigation/helpers/getParamsFromRoute.ts b/src/libs/Navigation/helpers/getParamsFromRoute.ts new file mode 100644 index 000000000000..1dd815f65e9b --- /dev/null +++ b/src/libs/Navigation/helpers/getParamsFromRoute.ts @@ -0,0 +1,12 @@ +import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; +import type {Screen} from '@src/SCREENS'; + +function getParamsFromRoute(screenName: string): string[] { + const routeConfig = normalizedConfigs[screenName as Screen]; + + const route = routeConfig.pattern; + + return route.match(/(?<=[:?&])(\w+)(?=[/=?&]|$)/g) ?? []; +} + +export default getParamsFromRoute; diff --git a/src/libs/Navigation/helpers/getPolicyIDFromState.ts b/src/libs/Navigation/helpers/getPolicyIDFromState.ts new file mode 100644 index 000000000000..808455834247 --- /dev/null +++ b/src/libs/Navigation/helpers/getPolicyIDFromState.ts @@ -0,0 +1,26 @@ +import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import extractPolicyIDFromQuery from './extractPolicyIDFromQuery'; + +/** + * returns policyID value if one exists in navigation state + * + * PolicyID in this app can be stored in two ways: + * - on NAVIGATORS.REPORTS_SPLIT_NAVIGATOR as `policyID` param + * - on Search related screens as policyID filter inside `q` (SearchQuery) param (only for SEARCH_CENTRAL_PANE) + */ +const getPolicyIDFromState = (state: State): string | undefined => { + const lastPolicyRoute = state?.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR || route.name === SCREENS.SEARCH.CENTRAL_PANE); + if (lastPolicyRoute?.params && 'policyID' in lastPolicyRoute.params) { + return lastPolicyRoute?.params?.policyID; + } + + if (lastPolicyRoute) { + return extractPolicyIDFromQuery(lastPolicyRoute as NavigationPartialRoute); + } + + return undefined; +}; + +export default getPolicyIDFromState; diff --git a/src/libs/Navigation/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts similarity index 93% rename from src/libs/Navigation/getStateFromPath.ts rename to src/libs/Navigation/helpers/getStateFromPath.ts index 50254bb3898d..19272ca3938f 100644 --- a/src/libs/Navigation/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -1,7 +1,7 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; +import linkingConfig from '@libs/Navigation/linkingConfig'; import type {Route} from '@src/ROUTES'; -import linkingConfig from './linkingConfig'; /** * @param path - The path to parse diff --git a/src/libs/Navigation/helpers/getTopmostReportParams.ts b/src/libs/Navigation/helpers/getTopmostReportParams.ts new file mode 100644 index 000000000000..618b8760add4 --- /dev/null +++ b/src/libs/Navigation/helpers/getTopmostReportParams.ts @@ -0,0 +1,37 @@ +import type {NavigationState, PartialState} from '@react-navigation/native'; +import type {ReportsSplitNavigatorParamList, RootStackParamList} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +// This function is in a separate file than Navigation.ts to avoid cyclic dependency. + +/** + * Find the last visited report screen in the navigation state and get its params. + * + * @param state - The react-navigation state + * @returns - It's possible that there is no report screen + */ + +type State = NavigationState | NavigationState | PartialState; + +function getTopmostReportParams(state: State): ReportsSplitNavigatorParamList[typeof SCREENS.REPORT] | undefined { + if (!state) { + return; + } + + const topmostReportsSplitNavigator = state.routes?.filter((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR).at(-1); + + if (!topmostReportsSplitNavigator) { + return; + } + + const topmostReport = topmostReportsSplitNavigator.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1); + + if (!topmostReport) { + return; + } + + return topmostReport?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]; +} + +export default getTopmostReportParams; diff --git a/src/libs/Navigation/getTopmostRouteName.ts b/src/libs/Navigation/helpers/getTopmostRouteName.ts similarity index 100% rename from src/libs/Navigation/getTopmostRouteName.ts rename to src/libs/Navigation/helpers/getTopmostRouteName.ts diff --git a/src/libs/Navigation/helpers/index.ts b/src/libs/Navigation/helpers/index.ts new file mode 100644 index 000000000000..71ef89bedf7e --- /dev/null +++ b/src/libs/Navigation/helpers/index.ts @@ -0,0 +1,25 @@ +export * from './createNormalizedConfigs'; +export * from './isNavigatorName'; +export * from './linkTo/types'; +export {default as closeRHPFlow} from './closeRHPFlow'; +export {default as createNormalizedConfigs} from './createNormalizedConfigs'; +export {default as customGetPathFromState} from './customGetPathFromState'; +export {default as extractPolicyIDFromQuery} from './extractPolicyIDFromQuery'; +export {default as getAdaptedStateFromPath} from './getAdaptedStateFromPath'; +export {default as getOnboardingAdaptedState} from './getOnboardingAdaptedState'; +export {default as getParamsFromRoute} from './getParamsFromRoute'; +export {default as getPolicyIDFromState} from './getPolicyIDFromState'; +export {default as getStateFromPath} from './getStateFromPath'; +export {default as getTopmostReportParams} from './getTopmostReportParams'; +export {default as getTopmostRouteName} from './getTopmostRouteName'; +export {default as isReportOpenInRHP} from './isReportOpenInRHP'; +export {default as isSearchTopmostFullScreenRoute} from './isSearchTopmostFullScreenRoute'; +export {default as isSideModalNavigator} from './isSideModalNavigator'; +export {default as linkTo} from './linkTo'; +export {default as getMinimalAction} from './linkTo/getMinimalAction'; +export {default as normalizePath} from './normalizePath'; +export {default as replacePathInNestedState} from './replacePathInNestedState'; +export {default as setNavigationActionToMicrotaskQueue} from './setNavigationActionToMicrotaskQueue'; +export {default as setupCustomAndroidBackHandler} from './setupCustomAndroidBackHandler'; +export {default as shouldOpenOnAdminRoom} from './shouldOpenOnAdminRoom'; +export {default as shouldPreventDeeplinkPrompt} from './shouldPreventDeeplinkPrompt'; diff --git a/src/libs/Navigation/helpers/isNavigatorName.ts b/src/libs/Navigation/helpers/isNavigatorName.ts new file mode 100644 index 000000000000..b642526716c0 --- /dev/null +++ b/src/libs/Navigation/helpers/isNavigatorName.ts @@ -0,0 +1,46 @@ +import RELATIONS from '@libs/Navigation/linkingConfig/RELATIONS'; +import type {FullScreenName, OnboardingFlowName, SplitNavigatorName, SplitNavigatorSidebarScreen} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; + +const ONBOARDING_SCREENS = [ + SCREENS.ONBOARDING.PERSONAL_DETAILS, + SCREENS.ONBOARDING.PURPOSE, + SCREENS.ONBOARDING_MODAL.ONBOARDING, + SCREENS.ONBOARDING.EMPLOYEES, + SCREENS.ONBOARDING.ACCOUNTING, +]; + +const SPLIT_NAVIGATORS_SET = new Set(Object.values(RELATIONS.SIDEBAR_TO_SPLIT)); +const FULL_SCREENS_SET = new Set([...Object.values(RELATIONS.SIDEBAR_TO_SPLIT), SCREENS.SEARCH.CENTRAL_PANE]); +const SIDEBARS_SET = new Set(Object.values(RELATIONS.SPLIT_TO_SIDEBAR)); +const ONBOARDING_SCREENS_SET = new Set(ONBOARDING_SCREENS); + +/** + * Functions defined below are used to check whether a screen belongs to a specific group. + * It is mainly used to filter routes in the navigation state. + */ +function checkIfScreenHasMatchingNameToSetValues(screen: string | undefined, set: Set): screen is T { + if (!screen) { + return false; + } + + return set.has(screen as T); +} + +function isOnboardingFlowName(screen: string | undefined) { + return checkIfScreenHasMatchingNameToSetValues(screen, ONBOARDING_SCREENS_SET); +} + +function isSplitNavigatorName(screen: string | undefined) { + return checkIfScreenHasMatchingNameToSetValues(screen, SPLIT_NAVIGATORS_SET); +} + +function isFullScreenName(screen: string | undefined) { + return checkIfScreenHasMatchingNameToSetValues(screen, FULL_SCREENS_SET); +} + +function isSidebarScreenName(screen: string | undefined) { + return checkIfScreenHasMatchingNameToSetValues(screen, SIDEBARS_SET); +} + +export {isFullScreenName, isOnboardingFlowName, isSidebarScreenName, isSplitNavigatorName}; diff --git a/src/libs/Navigation/isReportOpenInRHP.ts b/src/libs/Navigation/helpers/isReportOpenInRHP.ts similarity index 92% rename from src/libs/Navigation/isReportOpenInRHP.ts rename to src/libs/Navigation/helpers/isReportOpenInRHP.ts index 51e8a95bb66b..6158c3ec9d04 100644 --- a/src/libs/Navigation/isReportOpenInRHP.ts +++ b/src/libs/Navigation/helpers/isReportOpenInRHP.ts @@ -2,6 +2,7 @@ import type {NavigationState} from '@react-navigation/native'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; +// Determines whether the report page is opened in RHP. const isReportOpenInRHP = (state: NavigationState | undefined): boolean => { const lastRoute = state?.routes?.at(-1); if (!lastRoute) { diff --git a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts new file mode 100644 index 000000000000..e98b253b74bd --- /dev/null +++ b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts @@ -0,0 +1,16 @@ +import {navigationRef} from '@libs/Navigation/Navigation'; +import type {RootStackParamList, State} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; +import {isFullScreenName} from './isNavigatorName'; + +const isSearchTopmostFullScreenRoute = (): boolean => { + const rootState = navigationRef.getRootState() as State; + + if (!rootState) { + return false; + } + + return rootState.routes.findLast((route) => isFullScreenName(route.name))?.name === SCREENS.SEARCH.CENTRAL_PANE; +}; + +export default isSearchTopmostFullScreenRoute; diff --git a/src/libs/Navigation/isSideModalNavigator.ts b/src/libs/Navigation/helpers/isSideModalNavigator.ts similarity index 100% rename from src/libs/Navigation/isSideModalNavigator.ts rename to src/libs/Navigation/helpers/isSideModalNavigator.ts diff --git a/src/libs/Navigation/linkTo/getMinimalAction.ts b/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts similarity index 88% rename from src/libs/Navigation/linkTo/getMinimalAction.ts rename to src/libs/Navigation/helpers/linkTo/getMinimalAction.ts index ff01b3b8333b..9eab2f6f8717 100644 --- a/src/libs/Navigation/linkTo/getMinimalAction.ts +++ b/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts @@ -3,6 +3,11 @@ import type {Writable} from 'type-fest'; import type {State} from '@navigation/types'; import type {ActionPayload} from './types'; +type MinimalAction = { + action: Writable; + targetState: State | undefined; +}; + /** * Motivation for this function is described in NAVIGATION.md * @@ -10,7 +15,7 @@ import type {ActionPayload} from './types'; * @param state The root state * @returns minimalAction minimal action is the action that we should dispatch */ -function getMinimalAction(action: NavigationAction, state: NavigationState): Writable { +function getMinimalAction(action: NavigationAction, state: NavigationState): MinimalAction { let currentAction: NavigationAction = action; let currentState: State | undefined = state; let currentTargetKey: string | undefined; @@ -36,7 +41,7 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Wri target: currentTargetKey, }; } - return currentAction; + return {action: currentAction, targetState: currentState}; } export default getMinimalAction; diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts new file mode 100644 index 000000000000..39dd6b0b1571 --- /dev/null +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -0,0 +1,143 @@ +import {getActionFromState} from '@react-navigation/core'; +import type {NavigationContainerRef, NavigationState, PartialState, StackActionType} from '@react-navigation/native'; +import {findFocusedRoute, StackActions} from '@react-navigation/native'; +import {getMatchingFullScreenRoute, isFullScreenName} from '@libs/Navigation/helpers/getAdaptedStateFromPath'; +import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath'; +import normalizePath from '@libs/Navigation/helpers/normalizePath'; +import {shallowCompare} from '@libs/ObjectUtils'; +import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; +import linkingConfig from '@navigation/linkingConfig'; +import type {NavigationPartialRoute, ReportsSplitNavigatorParamList, RootStackParamList, StackNavigationAction} from '@navigation/types'; +import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; +import type {Route} from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import getMinimalAction from './getMinimalAction'; + +function createActionWithPolicyID(action: StackActionType, policyID: string): StackActionType | undefined { + if (action.type !== 'PUSH' && action.type !== 'REPLACE') { + return; + } + + return { + ...action, + payload: { + ...action.payload, + params: { + ...action.payload.params, + policyID, + }, + }, + }; +} + +function areNamesAndParamsEqual(currentState: NavigationState, stateFromPath: PartialState>) { + const currentFocusedRoute = findFocusedRoute(currentState); + const targetFocusedRoute = findFocusedRoute(stateFromPath); + + const areNamesEqual = currentFocusedRoute?.name === targetFocusedRoute?.name; + const areParamsEqual = shallowCompare(currentFocusedRoute?.params as Record | undefined, targetFocusedRoute?.params as Record | undefined); + + return areNamesEqual && areParamsEqual; +} + +function shouldCheckFullScreenRouteMatching(action: StackNavigationAction): action is StackNavigationAction & {type: 'PUSH'; payload: {name: typeof NAVIGATORS.RIGHT_MODAL_NAVIGATOR}} { + return action !== undefined && action.type === 'PUSH' && action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; +} + +function isNavigatingToAttachmentScreen(focusedRouteName?: string) { + return focusedRouteName === SCREENS.ATTACHMENTS; +} + +function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRoute, newRoute: NavigationPartialRoute) { + if (currentRoute.name !== SCREENS.REPORT || newRoute.name !== SCREENS.REPORT) { + return false; + } + + const currentParams = currentRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]; + const newParams = newRoute?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]; + + return currentParams.reportID === newParams.reportID; +} + +export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string) { + if (!navigation) { + throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); + } + + const normalizedPath = normalizePath(path); + const extractedPolicyID = extractPolicyIDFromPath(normalizedPath); + const pathWithoutPolicyID = getPathWithoutPolicyID(normalizedPath) as Route; + + // This is the state generated with the default getStateFromPath function. + // It won't include the whole state that will be generated for this path but the focused route will be correct. + // It is necessary because getActionFromState will generate RESET action for whole state generated with our custom getStateFromPath function. + const stateFromPath = getStateFromPath(pathWithoutPolicyID) as PartialState>; + const currentState = navigation.getRootState() as NavigationState; + + const focusedRouteFromPath = findFocusedRoute(stateFromPath); + const currentFocusedRoute = findFocusedRoute(currentState); + + // For type safety. It shouldn't ever happen. + if (!focusedRouteFromPath || !currentFocusedRoute) { + return; + } + + const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config); + + // If there is no action, just reset the whole state. + if (!action) { + navigation.resetRoot(stateFromPath); + return; + } + + // We don't want to dispatch action to push/replace with exactly the same route that is already focused. + if (areNamesAndParamsEqual(currentState, stateFromPath)) { + return; + } + + if (type === CONST.NAVIGATION.ACTION_TYPE.REPLACE) { + action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; + } + + // Attachment screen - This is a special case. We want to navigate to it instead of push. If there is no screen on the stack, it will be pushed. + // If not, it will be replaced. This way, navigating between one attachment screen and another won't be added to the browser history. + // Report screen - Also a special case. If we are navigating to the report with same reportID we want to replace it (navigate will do that). + // This covers the case when we open a specific message in report (reportActionID). + else if ( + action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE && + !isNavigatingToAttachmentScreen(focusedRouteFromPath?.name) && + !isNavigatingToReportWithSameReportID(currentFocusedRoute, focusedRouteFromPath) + ) { + // We want to PUSH by default to add entries to the browser history. + action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; + } + + // Handle deep links including policyID as /w/:policyID. + if (extractedPolicyID) { + const actionWithPolicyID = createActionWithPolicyID(action as StackActionType, extractedPolicyID); + if (!actionWithPolicyID) { + return; + } + + navigation.dispatch(actionWithPolicyID); + return; + } + + // If we deep link to a RHP page, we want to make sure we have the correct full screen route under the overlay. + if (shouldCheckFullScreenRouteMatching(action)) { + const newFocusedRoute = findFocusedRoute(stateFromPath); + if (newFocusedRoute) { + const matchingFullScreenRoute = getMatchingFullScreenRoute(newFocusedRoute); + + const lastFullScreenRoute = currentState.routes.findLast((route) => isFullScreenName(route.name)); + if (matchingFullScreenRoute && lastFullScreenRoute && matchingFullScreenRoute.name !== lastFullScreenRoute.name) { + const additionalAction = StackActions.push(matchingFullScreenRoute.name, {screen: matchingFullScreenRoute.state?.routes?.at(-1)?.name}); + navigation.dispatch(additionalAction); + } + } + } + + const {action: minimalAction} = getMinimalAction(action, navigation.getRootState()); + navigation.dispatch(minimalAction); +} diff --git a/src/libs/Navigation/linkTo/types.ts b/src/libs/Navigation/helpers/linkTo/types.ts similarity index 100% rename from src/libs/Navigation/linkTo/types.ts rename to src/libs/Navigation/helpers/linkTo/types.ts diff --git a/src/libs/Navigation/helpers/normalizePath.ts b/src/libs/Navigation/helpers/normalizePath.ts new file mode 100644 index 000000000000..9f15f95a540e --- /dev/null +++ b/src/libs/Navigation/helpers/normalizePath.ts @@ -0,0 +1,6 @@ +// Expensify uses path with leading '/' but react-navigation doesn't. This function normalizes the path to add the leading '/' for consistency. +function normalizePath(path: string) { + return !path.startsWith('/') ? `/${path}` : path; +} + +export default normalizePath; diff --git a/src/libs/Navigation/linkingConfig/replacePathInNestedState.ts b/src/libs/Navigation/helpers/replacePathInNestedState.ts similarity index 100% rename from src/libs/Navigation/linkingConfig/replacePathInNestedState.ts rename to src/libs/Navigation/helpers/replacePathInNestedState.ts diff --git a/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts b/src/libs/Navigation/helpers/setNavigationActionToMicrotaskQueue.ts similarity index 100% rename from src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts rename to src/libs/Navigation/helpers/setNavigationActionToMicrotaskQueue.ts diff --git a/src/libs/Navigation/helpers/setupCustomAndroidBackHandler/index.android.ts b/src/libs/Navigation/helpers/setupCustomAndroidBackHandler/index.android.ts new file mode 100644 index 000000000000..54b16e09947e --- /dev/null +++ b/src/libs/Navigation/helpers/setupCustomAndroidBackHandler/index.android.ts @@ -0,0 +1,20 @@ +import {BackHandler, NativeModules} from 'react-native'; +import navigationRef from '@navigation/navigationRef'; + +// We need to do some custom handling for the back button on Android for actions related to the hybrid app. +function setupCustomAndroidBackHandler() { + const onBackPress = () => { + const rootState = navigationRef.getRootState(); + const isLastScreenOnStack = rootState?.routes?.length === 1 && (rootState?.routes.at(0)?.state?.routes?.length ?? 1) === 1; + if (NativeModules.HybridAppModule && isLastScreenOnStack) { + NativeModules.HybridAppModule.exitApp(); + } + + // Handle all other cases with default handler. + return false; + }; + + BackHandler.addEventListener('hardwareBackPress', onBackPress); +} + +export default setupCustomAndroidBackHandler; diff --git a/src/libs/Navigation/setupCustomAndroidBackHandler/index.ts b/src/libs/Navigation/helpers/setupCustomAndroidBackHandler/index.ts similarity index 100% rename from src/libs/Navigation/setupCustomAndroidBackHandler/index.ts rename to src/libs/Navigation/helpers/setupCustomAndroidBackHandler/index.ts diff --git a/src/libs/Navigation/shouldOpenOnAdminRoom.ts b/src/libs/Navigation/helpers/shouldOpenOnAdminRoom.ts similarity index 75% rename from src/libs/Navigation/shouldOpenOnAdminRoom.ts rename to src/libs/Navigation/helpers/shouldOpenOnAdminRoom.ts index a593e8c22768..ae316fa3fa44 100644 --- a/src/libs/Navigation/shouldOpenOnAdminRoom.ts +++ b/src/libs/Navigation/helpers/shouldOpenOnAdminRoom.ts @@ -1,4 +1,4 @@ -import getCurrentUrl from './currentUrl'; +import getCurrentUrl from '@libs/Navigation/currentUrl'; export default function shouldOpenOnAdminRoom() { const url = getCurrentUrl(); diff --git a/src/libs/Navigation/shouldPreventDeeplinkPrompt.ts b/src/libs/Navigation/helpers/shouldPreventDeeplinkPrompt.ts similarity index 100% rename from src/libs/Navigation/shouldPreventDeeplinkPrompt.ts rename to src/libs/Navigation/helpers/shouldPreventDeeplinkPrompt.ts diff --git a/src/libs/Navigation/isSearchTopmostCentralPane.ts b/src/libs/Navigation/isSearchTopmostCentralPane.ts deleted file mode 100644 index 58eaf17a1be8..000000000000 --- a/src/libs/Navigation/isSearchTopmostCentralPane.ts +++ /dev/null @@ -1,17 +0,0 @@ -import SCREENS from '@src/SCREENS'; -import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; -import {navigationRef} from './Navigation'; -import type {RootStackParamList, State} from './types'; - -const isSearchTopmostCentralPane = (): boolean => { - const rootState = navigationRef.getRootState() as State; - - if (!rootState) { - return false; - } - - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState); - return topmostCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE; -}; - -export default isSearchTopmostCentralPane; diff --git a/src/libs/Navigation/linkTo/getActionForBottomTabNavigator.ts b/src/libs/Navigation/linkTo/getActionForBottomTabNavigator.ts deleted file mode 100644 index 85580d068ad7..000000000000 --- a/src/libs/Navigation/linkTo/getActionForBottomTabNavigator.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type {NavigationAction, NavigationState} from '@react-navigation/native'; -import type {Writable} from 'type-fest'; -import type {RootStackParamList, StackNavigationAction} from '@libs/Navigation/types'; -import getTopmostBottomTabRoute from '@navigation/getTopmostBottomTabRoute'; -import CONST from '@src/CONST'; -import type {ActionPayloadParams} from './types'; - -// Because we need to change the type to push, we also need to set target for this action to the bottom tab navigator. -function getActionForBottomTabNavigator( - action: StackNavigationAction, - state: NavigationState, - policyID?: string, - shouldNavigate?: boolean, -): Writable | undefined { - const bottomTabNavigatorRoute = state.routes.at(0); - if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.state === undefined || !action || action.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { - return; - } - - const params = action.payload.params as ActionPayloadParams; - let payloadParams = params.params as Record; - const screen = params.screen; - - if (policyID && !payloadParams?.policyID) { - payloadParams = {...payloadParams, policyID}; - } else if (!policyID) { - delete payloadParams?.policyID; - } - - // Check if the current bottom tab is the same as the one we want to navigate to. If it is, we don't need to do anything. - const bottomTabCurrentTab = getTopmostBottomTabRoute(state); - const bottomTabParams = bottomTabCurrentTab?.params as Record; - - // Verify if the policyID is different than the one we are currently on. If it is, we need to navigate to the new policyID. - const isNewPolicy = bottomTabParams?.policyID !== payloadParams?.policyID; - if (bottomTabCurrentTab?.name === screen && !shouldNavigate && !isNewPolicy) { - return; - } - - return { - type: CONST.NAVIGATION.ACTION_TYPE.PUSH, - payload: { - name: screen, - params: payloadParams, - }, - target: bottomTabNavigatorRoute.state.key, - }; -} - -export default getActionForBottomTabNavigator; diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts deleted file mode 100644 index 3ca41846d2b4..000000000000 --- a/src/libs/Navigation/linkTo/index.ts +++ /dev/null @@ -1,223 +0,0 @@ -import {getActionFromState} from '@react-navigation/core'; -import type {NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; -import {findFocusedRoute} from '@react-navigation/native'; -import omitBy from 'lodash/omitBy'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import shallowCompare from '@libs/ObjectUtils'; -import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; -import getActionsFromPartialDiff from '@navigation/AppNavigator/getActionsFromPartialDiff'; -import getPartialStateDiff from '@navigation/AppNavigator/getPartialStateDiff'; -import dismissModal from '@navigation/dismissModal'; -import extractPolicyIDFromQuery from '@navigation/extractPolicyIDFromQuery'; -import extrapolateStateFromParams from '@navigation/extrapolateStateFromParams'; -import getPolicyIDFromState from '@navigation/getPolicyIDFromState'; -import getStateFromPath from '@navigation/getStateFromPath'; -import getTopmostBottomTabRoute from '@navigation/getTopmostBottomTabRoute'; -import getTopmostCentralPaneRoute from '@navigation/getTopmostCentralPaneRoute'; -import getTopmostReportId from '@navigation/getTopmostReportId'; -import isSideModalNavigator from '@navigation/isSideModalNavigator'; -import linkingConfig from '@navigation/linkingConfig'; -import getAdaptedStateFromPath from '@navigation/linkingConfig/getAdaptedStateFromPath'; -import getMatchingBottomTabRouteForState from '@navigation/linkingConfig/getMatchingBottomTabRouteForState'; -import getMatchingCentralPaneRouteForState from '@navigation/linkingConfig/getMatchingCentralPaneRouteForState'; -import replacePathInNestedState from '@navigation/linkingConfig/replacePathInNestedState'; -import type {NavigationRoot, RootStackParamList, StackNavigationAction, State} from '@navigation/types'; -import CONST from '@src/CONST'; -import NAVIGATORS from '@src/NAVIGATORS'; -import type {Route} from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; -import getActionForBottomTabNavigator from './getActionForBottomTabNavigator'; -import getMinimalAction from './getMinimalAction'; -import type {ActionPayloadParams} from './types'; - -export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string, isActiveRoute?: boolean) { - if (!navigation) { - throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); - } - let root: NavigationRoot = navigation; - let current: NavigationRoot | undefined; - // Traverse up to get the root navigation - // eslint-disable-next-line no-cond-assign - while ((current = root.getParent())) { - root = current; - } - - const pathWithoutPolicyID = getPathWithoutPolicyID(`/${path}`) as Route; - const rootState = navigation.getRootState() as NavigationState; - const stateFromPath = getStateFromPath(pathWithoutPolicyID) as PartialState>; - // Creating path with /w/ included if necessary. - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState); - - const extractedPolicyID = extractPolicyIDFromPath(`/${path}`); - const policyIDFromState = getPolicyIDFromState(rootState); - const policyID = extractedPolicyID ?? policyIDFromState; - const lastRoute = rootState?.routes?.at(-1); - - const isNarrowLayout = getIsNarrowLayout(); - - const isFullScreenOnTop = lastRoute?.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR; - - // policyID on SCREENS.SEARCH.CENTRAL_PANE can be present only as part of SearchQuery, while on other pages it's stored in the url in the format: /w/:policyID/ - if (policyID && !isFullScreenOnTop && !policyIDFromState) { - // The stateFromPath doesn't include proper path if there is a policy passed with /w/id. - // We need to replace the path in the state with the proper one. - // To avoid this hacky solution we may want to create custom getActionFromState function in the future. - replacePathInNestedState(stateFromPath, `/w/${policyID}${pathWithoutPolicyID}`); - } - - const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config); - - const isReportInRhpOpened = isReportOpenInRHP(rootState); - - // If action type is different than NAVIGATE we can't change it to the PUSH safely - if (action?.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { - const actionPayloadParams = action.payload.params as ActionPayloadParams; - - const topRouteName = lastRoute?.name; - - // CentralPane screens aren't nested in any navigator, if actionPayloadParams?.screen is undefined, it means the screen name and parameters have to be read directly from action.payload - const targetName = actionPayloadParams?.screen ?? action.payload.name; - const targetParams = actionPayloadParams?.params ?? actionPayloadParams; - const isTargetNavigatorOnTop = topRouteName === action.payload.name; - - const isTargetScreenDifferentThanCurrent = !!(!topmostCentralPaneRoute || topmostCentralPaneRoute.name !== targetName); - const areParamsDifferent = - targetName === SCREENS.REPORT - ? getTopmostReportId(rootState) !== getTopmostReportId(stateFromPath) - : !shallowCompare( - omitBy(topmostCentralPaneRoute?.params as Record | undefined, (value) => value === undefined), - omitBy(targetParams as Record | undefined, (value) => value === undefined), - ); - - // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack by default - if (isCentralPaneName(action.payload.name) && (isTargetScreenDifferentThanCurrent || areParamsDifferent)) { - // We need to push a tab if the tab doesn't match the central pane route that we are going to push. - const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState); - - const focusedRoute = findFocusedRoute(stateFromPath); - const policyIDFromQuery = extractPolicyIDFromQuery(focusedRoute); - const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateFromPath, policyID ?? policyIDFromQuery); - const isOpeningSearch = matchingBottomTabRoute.name === SCREENS.SEARCH.BOTTOM_TAB; - const isNewPolicyID = - ((topmostBottomTabRoute?.params as Record)?.policyID ?? '') !== - ((matchingBottomTabRoute?.params as Record)?.policyID ?? ''); - - if (topmostBottomTabRoute && (topmostBottomTabRoute.name !== matchingBottomTabRoute.name || isNewPolicyID || isOpeningSearch)) { - root.dispatch({ - type: CONST.NAVIGATION.ACTION_TYPE.PUSH, - payload: matchingBottomTabRoute, - }); - } - - if (type === CONST.NAVIGATION.TYPE.UP) { - action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; - } else { - action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; - } - - // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow - // and at the same time we want the back button to go to the page we were before the deeplink - } else if (type === CONST.NAVIGATION.TYPE.UP) { - if (!areParamsDifferent && isSideModalNavigator(lastRoute?.name) && topmostCentralPaneRoute?.name === targetName) { - dismissModal(navigation); - return; - } - action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; - - // If this action is navigating to ModalNavigator or FullScreenNavigator and the last route on the root navigator is not already opened Navigator then push - } else if ((action.payload.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR || isSideModalNavigator(action.payload.name)) && !isTargetNavigatorOnTop) { - if (isSideModalNavigator(topRouteName)) { - dismissModal(navigation); - } - - // If this RHP has mandatory central pane and bottom tab screens defined we need to push them. - const {adaptedState, metainfo} = getAdaptedStateFromPath(path, linkingConfig.config); - if (adaptedState && (metainfo.isCentralPaneAndBottomTabMandatory || metainfo.isFullScreenNavigatorMandatory)) { - const diff = getPartialStateDiff(rootState, adaptedState as State, metainfo); - const diffActions = getActionsFromPartialDiff(diff); - for (const diffAction of diffActions) { - root.dispatch(diffAction); - } - } - // All actions related to FullScreenNavigator on wide screen are pushed when comparing differences between rootState and adaptedState. - if (action.payload.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) { - return; - } - action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - } else if (action.payload.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR) { - // If path contains a policyID, we should invoke the navigate function - const shouldNavigate = !!extractedPolicyID; - const actionForBottomTabNavigator = getActionForBottomTabNavigator(action, rootState, policyID, shouldNavigate); - - if (!actionForBottomTabNavigator) { - return; - } - - root.dispatch(actionForBottomTabNavigator); - - // If the layout is wide we need to push matching central pane route to the stack. - if (!isNarrowLayout) { - // stateFromPath should always include bottom tab navigator state, so getMatchingCentralPaneRouteForState will be always defined. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(stateFromPath, rootState)!; - if (matchingCentralPaneRoute && 'name' in matchingCentralPaneRoute) { - root.dispatch({ - type: CONST.NAVIGATION.ACTION_TYPE.PUSH, - payload: { - name: matchingCentralPaneRoute.name, - params: matchingCentralPaneRoute.params, - }, - }); - } - } else { - // If the layout is small we need to pop everything from the central pane so the bottom tab navigator is visible. - root.dispatch({ - type: 'POP_TO_TOP', - target: rootState.key, - }); - } - return; - } - } - - if (action && 'payload' in action && action.payload && 'name' in action.payload && isSideModalNavigator(action.payload.name)) { - // Information about the state may be in the params. - const currentFocusedRoute = findFocusedRoute(extrapolateStateFromParams(rootState)); - const targetFocusedRoute = findFocusedRoute(stateFromPath); - - // If the current focused route is the same as the target focused route, we don't want to navigate. - if ( - currentFocusedRoute?.name === targetFocusedRoute?.name && - shallowCompare(currentFocusedRoute?.params as Record, targetFocusedRoute?.params as Record) - ) { - return; - } - - const minimalAction = getMinimalAction(action, navigation.getRootState()); - if (minimalAction) { - // There are situations where a route already exists on the current navigation stack - // But we want to push the same route instead of going back in the stack - // Which would break the user navigation history - if (!isActiveRoute && type === CONST.NAVIGATION.ACTION_TYPE.PUSH) { - minimalAction.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; - } - root.dispatch(minimalAction); - return; - } - } - - // When we navigate from the ReportScreen opened in RHP, this page shouldn't be removed from the navigation state to allow users to go back to it. - if (isReportInRhpOpened && action) { - action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; - } - - if (action !== undefined) { - root.dispatch(action); - } else { - root.reset(stateFromPath); - } -} diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts new file mode 100644 index 000000000000..feee223c233c --- /dev/null +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -0,0 +1,31 @@ +import SCREENS from '@src/SCREENS'; + +// This file is used to define RHP screens that are in relation to the search screen. +const SEARCH_TO_RHP: string[] = [ + SCREENS.SEARCH.REPORT_RHP, + SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_SUBMITTED_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_APPROVED_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_PAID_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_EXPORTED_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_POSTED_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP, + SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP, +]; + +export default SEARCH_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts similarity index 67% rename from src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts rename to src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 96d3d8b74080..1df3af0a6e86 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -1,7 +1,8 @@ -import type {CentralPaneName} from '@libs/Navigation/types'; +import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; -const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = { +// This file is used to define relation between settings split navigator's central screens and RHP screens. +const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = { [SCREENS.SETTINGS.PROFILE.ROOT]: [ SCREENS.SETTINGS.PROFILE.DISPLAY_NAME, SCREENS.SETTINGS.PROFILE.CONTACT_METHODS, @@ -50,32 +51,6 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS], [SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER], [SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE], - [SCREENS.SEARCH.CENTRAL_PANE]: [ - SCREENS.SEARCH.REPORT_RHP, - SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_SUBMITTED_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_APPROVED_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_PAID_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_EXPORTED_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_POSTED_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP, - SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP, - SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP, - ], [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [ SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_RHP.ts new file mode 100644 index 000000000000..4deffa6fd876 --- /dev/null +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_RHP.ts @@ -0,0 +1,19 @@ +import type {SplitNavigatorSidebarScreen} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; + +/** + * This file is used to define the relationship between the sidebar and the right hand pane (RHP) screen. + * This means that going back from RHP will take the user directly to the sidebar. On wide layout the default central screen will be used to fill the space. + */ +const SIDEBAR_TO_RHP: Partial> = { + [SCREENS.SETTINGS.ROOT]: [ + SCREENS.SETTINGS.SHARE_CODE, + SCREENS.SETTINGS.PROFILE.STATUS, + SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, + SCREENS.SETTINGS.EXIT_SURVEY.REASON, + SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE, + SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM, + ], +}; + +export default SIDEBAR_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts new file mode 100644 index 000000000000..c4d18632ca68 --- /dev/null +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts @@ -0,0 +1,11 @@ +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +// This file is used to define the relationship between the sidebar (LHN) and the parent split navigator. +const SIDEBAR_TO_SPLIT = { + [SCREENS.SETTINGS.ROOT]: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, + [SCREENS.HOME]: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, + [SCREENS.WORKSPACE.INITIAL]: NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, +}; + +export default SIDEBAR_TO_SPLIT; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts similarity index 97% rename from src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts rename to src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index f36f154819f5..1c4f684a9aed 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -1,7 +1,8 @@ -import type {FullScreenName} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; -const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { +// This file is used to define relation between workspace split navigator's central screens and RHP screens. +const WORKSPACE_TO_RHP: Partial> = { [SCREENS.WORKSPACE.PROFILE]: [ SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.ADDRESS, @@ -248,4 +249,4 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { [SCREENS.WORKSPACE.PER_DIEM]: [SCREENS.WORKSPACE.PER_DIEM_IMPORT, SCREENS.WORKSPACE.PER_DIEM_IMPORTED, SCREENS.WORKSPACE.PER_DIEM_SETTINGS], }; -export default FULL_SCREEN_TO_RHP_MAPPING; +export default WORKSPACE_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/index.ts b/src/libs/Navigation/linkingConfig/RELATIONS/index.ts new file mode 100644 index 000000000000..eed5e8d78b39 --- /dev/null +++ b/src/libs/Navigation/linkingConfig/RELATIONS/index.ts @@ -0,0 +1,29 @@ +import SEARCH_TO_RHP from './SEARCH_TO_RHP'; +import SETTINGS_TO_RHP from './SETTINGS_TO_RHP'; +import SIDEBAR_TO_RHP from './SIDEBAR_TO_RHP'; +import SIDEBAR_TO_SPLIT from './SIDEBAR_TO_SPLIT'; +import WORKSPACE_TO_RHP from './WORKSPACE_TO_RHP'; + +function createInverseRelation(relations: Partial>): Record { + const reversedRelations = {} as Record; + + Object.entries(relations).forEach(([key, values]) => { + const valuesWithType = (Array.isArray(values) ? values : [values]) as K[]; + valuesWithType.forEach((value: K) => { + reversedRelations[value] = key as T; + }); + }); + return reversedRelations; +} + +export default { + SETTINGS_TO_RHP, + RHP_TO_SETTINGS: createInverseRelation(SETTINGS_TO_RHP), + RHP_TO_WORKSPACE: createInverseRelation(WORKSPACE_TO_RHP), + RHP_TO_SIDEBAR: createInverseRelation(SIDEBAR_TO_RHP), + SEARCH_TO_RHP, + SIDEBAR_TO_RHP, + WORKSPACE_TO_RHP, + SIDEBAR_TO_SPLIT, + SPLIT_TO_SIDEBAR: createInverseRelation(SIDEBAR_TO_SPLIT), +}; diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts deleted file mode 100755 index a68959ae7d0f..000000000000 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type {BottomTabName, CentralPaneName} from '@navigation/types'; -import SCREENS from '@src/SCREENS'; - -const TAB_TO_CENTRAL_PANE_MAPPING: Record = { - [SCREENS.HOME]: [SCREENS.REPORT], - [SCREENS.SEARCH.BOTTOM_TAB]: [SCREENS.SEARCH.CENTRAL_PANE], - [SCREENS.SETTINGS.ROOT]: [ - SCREENS.SETTINGS.PROFILE.ROOT, - SCREENS.SETTINGS.PREFERENCES.ROOT, - SCREENS.SETTINGS.SECURITY, - SCREENS.SETTINGS.WALLET.ROOT, - SCREENS.SETTINGS.ABOUT, - SCREENS.SETTINGS.WORKSPACES, - SCREENS.SETTINGS.SAVE_THE_WORLD, - SCREENS.SETTINGS.TROUBLESHOOT, - SCREENS.SETTINGS.SUBSCRIPTION.ROOT, - ], -}; - -const generateCentralPaneToTabMapping = (): Record => { - const mapping: Record = {} as Record; - for (const [tabName, CentralPaneNames] of Object.entries(TAB_TO_CENTRAL_PANE_MAPPING)) { - for (const CentralPaneName of CentralPaneNames) { - mapping[CentralPaneName] = tabName as BottomTabName; - } - } - return mapping; -}; - -const CENTRAL_PANE_TO_TAB_MAPPING: Record = generateCentralPaneToTabMapping(); - -export {CENTRAL_PANE_TO_TAB_MAPPING}; -export default TAB_TO_CENTRAL_PANE_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c9dd7f17ad86..18965cf94b62 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1,15 +1,14 @@ import type {LinkingOptions} from '@react-navigation/native'; +import {createNormalizedConfigs} from '@libs/Navigation/helpers'; +import type {RouteConfig} from '@libs/Navigation/helpers'; import type {RootStackParamList} from '@navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; -import type {RouteConfig} from './createNormalizedConfigs'; -import createNormalizedConfigs from './createNormalizedConfigs'; // Moved to a separate file to avoid cyclic dependencies. const config: LinkingOptions['config'] = { - initialRouteName: NAVIGATORS.BOTTOM_TAB_NAVIGATOR, screens: { // Main Routes [SCREENS.VALIDATE_LOGIN]: ROUTES.VALIDATE_LOGIN, @@ -29,49 +28,37 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route, [SCREENS.TRANSACTION_RECEIPT]: ROUTES.TRANSACTION_RECEIPT.route, [SCREENS.WORKSPACE_JOIN_USER]: ROUTES.WORKSPACE_JOIN_USER.route, - [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, - [SCREENS.SETTINGS.PROFILE.ROOT]: { - path: ROUTES.SETTINGS_PROFILE, - exact: true, - }, - [SCREENS.SETTINGS.PREFERENCES.ROOT]: { - path: ROUTES.SETTINGS_PREFERENCES, - exact: true, - }, - [SCREENS.SETTINGS.SECURITY]: { - path: ROUTES.SETTINGS_SECURITY, - exact: true, - }, - [SCREENS.SETTINGS.WALLET.ROOT]: { - path: ROUTES.SETTINGS_WALLET, - exact: true, - }, - [SCREENS.SETTINGS.ABOUT]: { - path: ROUTES.SETTINGS_ABOUT, - exact: true, - }, - [SCREENS.SETTINGS.TROUBLESHOOT]: { - path: ROUTES.SETTINGS_TROUBLESHOOT, - exact: true, - }, - [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, + // [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, + // [SCREENS.SETTINGS.PROFILE.ROOT]: { + // path: ROUTES.SETTINGS_PROFILE, + // exact: true, + // }, + // [SCREENS.SETTINGS.PREFERENCES.ROOT]: { + // path: ROUTES.SETTINGS_PREFERENCES, + // // exact: true, + // }, + // [SCREENS.SETTINGS.SECURITY]: { + // path: ROUTES.SETTINGS_SECURITY, + // exact: true, + // }, + // [SCREENS.SETTINGS.WALLET.ROOT]: { + // path: ROUTES.SETTINGS_WALLET, + // exact: true, + // }, + // [SCREENS.SETTINGS.ABOUT]: { + // path: ROUTES.SETTINGS_ABOUT, + // exact: true, + // }, + // [SCREENS.SETTINGS.TROUBLESHOOT]: { + // path: ROUTES.SETTINGS_TROUBLESHOOT, + // exact: true, + // }, + // [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, [SCREENS.SEARCH.CENTRAL_PANE]: { path: ROUTES.SEARCH_CENTRAL_PANE.route, }, - [SCREENS.SETTINGS.SAVE_THE_WORLD]: ROUTES.SETTINGS_SAVE_THE_WORLD, - [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: ROUTES.SETTINGS_SUBSCRIPTION, - - // Sidebar - [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: { - path: ROUTES.ROOT, - initialRouteName: SCREENS.HOME, - screens: { - [SCREENS.HOME]: ROUTES.HOME, - [SCREENS.SETTINGS.ROOT]: { - path: ROUTES.SETTINGS, - }, - }, - }, + // [SCREENS.SETTINGS.SAVE_THE_WORLD]: ROUTES.SETTINGS_SAVE_THE_WORLD, + // [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: ROUTES.SETTINGS_SUBSCRIPTION, [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { @@ -1476,7 +1463,57 @@ const config: LinkingOptions['config'] = { }, }, - [NAVIGATORS.FULL_SCREEN_NAVIGATOR]: { + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: { + path: ROUTES.ROOT, + screens: { + [SCREENS.HOME]: { + path: ROUTES.HOME, + exact: true, + }, + [SCREENS.REPORT]: { + path: ROUTES.REPORT_WITH_ID.route, + exact: true, + }, + }, + }, + + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: { + screens: { + [SCREENS.SETTINGS.ROOT]: ROUTES.SETTINGS, + [SCREENS.SETTINGS.WORKSPACES]: { + path: ROUTES.SETTINGS_WORKSPACES, + exact: true, + }, + [SCREENS.SETTINGS.PROFILE.ROOT]: { + path: ROUTES.SETTINGS_PROFILE, + exact: true, + }, + [SCREENS.SETTINGS.SECURITY]: { + path: ROUTES.SETTINGS_SECURITY, + exact: true, + }, + [SCREENS.SETTINGS.WALLET.ROOT]: { + path: ROUTES.SETTINGS_WALLET, + exact: true, + }, + [SCREENS.SETTINGS.ABOUT]: { + path: ROUTES.SETTINGS_ABOUT, + exact: true, + }, + [SCREENS.SETTINGS.TROUBLESHOOT]: { + path: ROUTES.SETTINGS_TROUBLESHOOT, + exact: true, + }, + [SCREENS.SETTINGS.SAVE_THE_WORLD]: ROUTES.SETTINGS_SAVE_THE_WORLD, + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: ROUTES.SETTINGS_SUBSCRIPTION, + [SCREENS.SETTINGS.PREFERENCES.ROOT]: { + path: ROUTES.SETTINGS_PREFERENCES, + // exact: true, + }, + }, + }, + + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: { screens: { [SCREENS.WORKSPACE.INITIAL]: { path: ROUTES.WORKSPACE_INITIAL.route, diff --git a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts deleted file mode 100644 index a9c9b6f23b19..000000000000 --- a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {getPathFromState} from '@react-navigation/native'; -import getPolicyIDFromState from '@libs/Navigation/getPolicyIDFromState'; -import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; -import type {BottomTabName, RootStackParamList, State} from '@libs/Navigation/types'; -import {removePolicyIDParamFromState} from '@libs/NavigationUtils'; -import SCREENS from '@src/SCREENS'; - -// The policy ID parameter should be included in the URL when any of these pages is opened in the bottom tab. -const SCREENS_WITH_POLICY_ID_IN_URL: BottomTabName[] = [SCREENS.HOME] as const; - -const customGetPathFromState: typeof getPathFromState = (state, options) => { - // For the Home and Settings pages we should remove policyID from the params, because on small screens it's displayed twice in the URL - const stateWithoutPolicyID = removePolicyIDParamFromState(state as State); - const path = getPathFromState(stateWithoutPolicyID, options); - const policyIDFromState = getPolicyIDFromState(state as State); - const topmostBottomTabRouteName = getTopmostBottomTabRoute(state as State)?.name; - const shouldAddPolicyID = !!topmostBottomTabRouteName && SCREENS_WITH_POLICY_ID_IN_URL.includes(topmostBottomTabRouteName); - return `${policyIDFromState && shouldAddPolicyID ? `/w/${policyIDFromState}` : ''}${path}`; -}; - -export default customGetPathFromState; diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts deleted file mode 100644 index b0fba017b367..000000000000 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ /dev/null @@ -1,410 +0,0 @@ -import type {NavigationState, PartialState, Route} from '@react-navigation/native'; -import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; -import pick from 'lodash/pick'; -import type {TupleToUnion} from 'type-fest'; -import type {TopTabScreen} from '@components/FocusTrap/TOP_TAB_SCREENS'; -import {isAnonymousUser} from '@libs/actions/Session'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; -import * as ReportConnection from '@libs/ReportConnection'; -import extractPolicyIDFromQuery from '@navigation/extractPolicyIDFromQuery'; -import CONST from '@src/CONST'; -import NAVIGATORS from '@src/NAVIGATORS'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Screen} from '@src/SCREENS'; -import SCREENS from '@src/SCREENS'; -import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; -import config, {normalizedConfigs} from './config'; -import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; -import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForState'; -import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteForState'; -import getOnboardingAdaptedState from './getOnboardingAdaptedState'; -import replacePathInNestedState from './replacePathInNestedState'; - -const RHP_SCREENS_OPENED_FROM_LHN = [ - SCREENS.SETTINGS.SHARE_CODE, - SCREENS.SETTINGS.PROFILE.STATUS, - SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, - SCREENS.MONEY_REQUEST.CREATE, - SCREENS.SETTINGS.EXIT_SURVEY.REASON, - SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE, - SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM, - CONST.TAB_REQUEST.DISTANCE, - CONST.TAB_REQUEST.MANUAL, - CONST.TAB_REQUEST.SCAN, -] satisfies Array; - -type RHPScreenOpenedFromLHN = TupleToUnion; - -type Metainfo = { - // Sometimes modal screens don't have information about what should be visible under the overlay. - // That means such screen can have different screens under the overlay depending on what was already in the state. - // If the screens in the bottom tab and central pane are not mandatory for this state, we want to have this information. - // It will help us later with creating proper diff betwen current and desired state. - isCentralPaneAndBottomTabMandatory: boolean; - isFullScreenNavigatorMandatory: boolean; -}; - -type GetAdaptedStateReturnType = { - adaptedState: ReturnType; - metainfo: Metainfo; -}; - -type GetAdaptedStateFromPath = (...args: [...Parameters, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType; - -// The function getPathFromState that we are using in some places isn't working correctly without defined index. -const getRoutesWithIndex = (routes: NavigationPartialRoute[]): PartialState => ({routes, index: routes.length - 1}); - -const addPolicyIDToRoute = (route: NavigationPartialRoute, policyID?: string) => { - const routeWithPolicyID = {...route}; - if (!routeWithPolicyID.params) { - routeWithPolicyID.params = {policyID}; - return routeWithPolicyID; - } - - if ('policyID' in routeWithPolicyID.params && !!routeWithPolicyID.params.policyID) { - return routeWithPolicyID; - } - - routeWithPolicyID.params = {...routeWithPolicyID.params, policyID}; - - return routeWithPolicyID; -}; - -function createBottomTabNavigator(route: NavigationPartialRoute, policyID?: string): NavigationPartialRoute { - const routesForBottomTabNavigator: Array> = []; - routesForBottomTabNavigator.push(addPolicyIDToRoute(route, policyID) as NavigationPartialRoute); - - return { - name: NAVIGATORS.BOTTOM_TAB_NAVIGATOR, - state: getRoutesWithIndex(routesForBottomTabNavigator), - }; -} - -function createFullScreenNavigator(route?: NavigationPartialRoute): NavigationPartialRoute { - const routes = []; - - const policyID = route?.params && 'policyID' in route.params ? route.params.policyID : undefined; - - // Both routes in FullScreenNavigator should store a policyID in params, so here this param is also passed to the screen displayed in LHN in FullScreenNavigator - routes.push({ - name: SCREENS.WORKSPACE.INITIAL, - params: { - policyID, - }, - }); - - if (route) { - routes.push(route); - } - return { - name: NAVIGATORS.FULL_SCREEN_NAVIGATOR, - state: getRoutesWithIndex(routes), - }; -} - -function getParamsFromRoute(screenName: string): string[] { - const routeConfig = normalizedConfigs[screenName as Screen]; - - const route = routeConfig.pattern; - - return route.match(/(?<=[:?&])(\w+)(?=[/=?&]|$)/g) ?? []; -} - -// This function will return CentralPaneNavigator route or FullScreenNavigator route. -function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): NavigationPartialRoute | undefined { - // Check for backTo param. One screen with different backTo value may need diferent screens visible under the overlay. - if (route.params && 'backTo' in route.params && typeof route.params.backTo === 'string') { - const stateForBackTo = getStateFromPath(route.params.backTo, config); - if (stateForBackTo) { - // If there is rhpNavigator in the state generated for backTo url, we want to get root route matching to this rhp screen. - const rhpNavigator = stateForBackTo.routes.find((rt) => rt.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); - if (rhpNavigator && rhpNavigator.state) { - return getMatchingRootRouteForRHPRoute(findFocusedRoute(stateForBackTo) as NavigationPartialRoute); - } - - // If we know that backTo targets the root route (full screen) we want to use it. - const fullScreenNavigator = stateForBackTo.routes.find((rt) => rt.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); - if (fullScreenNavigator && fullScreenNavigator.state) { - return fullScreenNavigator as NavigationPartialRoute; - } - - // If we know that backTo targets a central pane screen we want to use it. - const centralPaneScreen = stateForBackTo.routes.find((rt) => isCentralPaneName(rt.name)); - if (centralPaneScreen) { - return centralPaneScreen as NavigationPartialRoute; - } - } - } - - // Check for CentralPaneNavigator - for (const [centralPaneName, RHPNames] of Object.entries(CENTRAL_PANE_TO_RHP_MAPPING)) { - if (RHPNames.includes(route.name)) { - const paramsFromRoute = getParamsFromRoute(centralPaneName); - - return {name: centralPaneName as CentralPaneName, params: pick(route.params, paramsFromRoute)}; - } - } - - // Check for FullScreenNavigator - for (const [fullScreenName, RHPNames] of Object.entries(FULL_SCREEN_TO_RHP_MAPPING)) { - if (RHPNames.includes(route.name)) { - const paramsFromRoute = getParamsFromRoute(fullScreenName); - - return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: pick(route.params, paramsFromRoute)}); - } - } - - // check for valid reportID in the route params - // if the reportID is valid, we should navigate back to screen report in CPN - const reportID = (route.params as Record)?.reportID; - if (ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) { - return {name: SCREENS.REPORT, params: {reportID}}; - } -} - -function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType { - const isNarrowLayout = getIsNarrowLayout(); - const metainfo = { - isCentralPaneAndBottomTabMandatory: true, - isFullScreenNavigatorMandatory: true, - }; - - // We need to check what is defined to know what we need to add. - const bottomTabNavigator = state.routes.find((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); - const centralPaneNavigator = state.routes.find((route) => isCentralPaneName(route.name)); - const fullScreenNavigator = state.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); - const rhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); - const lhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR); - const onboardingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR); - const welcomeVideoModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR); - const migratedUserModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR); - const attachmentsScreen = state.routes.find((route) => route.name === SCREENS.ATTACHMENTS); - const featureTrainingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR); - - if (rhpNavigator) { - // Routes - // - matching bottom tab - // - matching root route for rhp - // - found rhp - - // This one will be defined because rhpNavigator is defined. - const focusedRHPRoute = findFocusedRoute(state); - const routes = []; - - if (focusedRHPRoute) { - let matchingRootRoute = getMatchingRootRouteForRHPRoute(focusedRHPRoute); - const isRHPScreenOpenedFromLHN = focusedRHPRoute?.name && RHP_SCREENS_OPENED_FROM_LHN.includes(focusedRHPRoute?.name as RHPScreenOpenedFromLHN); - // This may happen if this RHP doesn't have a route that should be under the overlay defined. - if (!matchingRootRoute || isRHPScreenOpenedFromLHN) { - metainfo.isCentralPaneAndBottomTabMandatory = false; - metainfo.isFullScreenNavigatorMandatory = false; - // If matchingRootRoute is undefined and it's a narrow layout, don't add a report screen under the RHP. - matchingRootRoute = matchingRootRoute ?? (!isNarrowLayout ? {name: SCREENS.REPORT} : undefined); - } - - // If the root route is type of FullScreenNavigator, the default bottom tab will be added. - const matchingBottomTabRoute = getMatchingBottomTabRouteForState({routes: matchingRootRoute ? [matchingRootRoute] : []}); - routes.push(createBottomTabNavigator(matchingBottomTabRoute, policyID)); - // When we open a screen in RHP from FullScreenNavigator, we need to add the appropriate screen in CentralPane. - // Then, when we close FullScreenNavigator, we will be redirected to the correct page in CentralPane. - if (matchingRootRoute?.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) { - routes.push({name: SCREENS.SETTINGS.WORKSPACES}); - } - - if (matchingRootRoute && (!isNarrowLayout || !isRHPScreenOpenedFromLHN)) { - routes.push(matchingRootRoute); - } - } - - routes.push(rhpNavigator); - return { - adaptedState: getRoutesWithIndex(routes), - metainfo, - }; - } - if (lhpNavigator ?? onboardingModalNavigator ?? welcomeVideoModalNavigator ?? featureTrainingModalNavigator ?? migratedUserModalNavigator) { - // Routes - // - default bottom tab - // - default central pane on desktop layout - // - found lhp / onboardingModalNavigator - - // There is no screen in these navigators that would have mandatory central pane, bottom tab or fullscreen navigator. - metainfo.isCentralPaneAndBottomTabMandatory = false; - metainfo.isFullScreenNavigatorMandatory = false; - const routes = []; - routes.push( - createBottomTabNavigator( - { - name: SCREENS.HOME, - }, - policyID, - ), - ); - if (!isNarrowLayout) { - routes.push({ - name: SCREENS.REPORT, - }); - } - - // Separate ifs are necessary for typescript to see that we are not pushing undefined to the array. - if (lhpNavigator) { - routes.push(lhpNavigator); - } - - if (onboardingModalNavigator) { - if (onboardingModalNavigator.state) { - // Build the routes list based on the current onboarding step, so going back will go to the previous step instead of closing the onboarding flow - routes.push({ - ...onboardingModalNavigator, - state: getOnboardingAdaptedState(onboardingModalNavigator.state), - }); - } else { - routes.push(onboardingModalNavigator); - } - } - - if (welcomeVideoModalNavigator) { - routes.push(welcomeVideoModalNavigator); - } - - if (migratedUserModalNavigator) { - routes.push(migratedUserModalNavigator); - } - - if (featureTrainingModalNavigator) { - routes.push(featureTrainingModalNavigator); - } - - return { - adaptedState: getRoutesWithIndex(routes), - metainfo, - }; - } - if (fullScreenNavigator) { - // Routes - // - default bottom tab - // - default central pane on desktop layout - // - found fullscreen - - const routes = []; - routes.push( - createBottomTabNavigator( - { - name: SCREENS.SETTINGS.ROOT, - }, - policyID, - ), - ); - - routes.push({ - name: SCREENS.SETTINGS.WORKSPACES, - }); - - routes.push(fullScreenNavigator); - - return { - adaptedState: getRoutesWithIndex(routes), - metainfo, - }; - } - if (centralPaneNavigator) { - // Routes - // - matching bottom tab - // - found central pane - const routes = []; - const matchingBottomTabRoute = getMatchingBottomTabRouteForState(state); - routes.push(createBottomTabNavigator(matchingBottomTabRoute, policyID)); - routes.push(centralPaneNavigator); - - return { - adaptedState: getRoutesWithIndex(routes), - metainfo, - }; - } - if (attachmentsScreen) { - // Routes - // - matching bottom tab - // - central pane (report screen) of the attachment - // - found report attachments - const routes = []; - const reportAttachments = attachmentsScreen as Route<'Attachments', RootStackParamList['Attachments']>; - - if (reportAttachments.params?.type === CONST.ATTACHMENT_TYPE.REPORT) { - const matchingBottomTabRoute = getMatchingBottomTabRouteForState(state); - routes.push(createBottomTabNavigator(matchingBottomTabRoute, policyID)); - if (!isNarrowLayout) { - routes.push({name: SCREENS.REPORT, params: {reportID: reportAttachments.params?.reportID ?? '-1'}}); - } - routes.push(reportAttachments); - - return { - adaptedState: getRoutesWithIndex(routes), - metainfo, - }; - } - } - - // We need to make sure that this if only handles states where we deeplink to the bottom tab directly - if (bottomTabNavigator && bottomTabNavigator.state) { - // Routes - // - found bottom tab - // - matching central pane on desktop layout - - // We want to make sure that the bottom tab search page is always pushed with matching central pane page. Even on the narrow layout. - if (isNarrowLayout && bottomTabNavigator.state?.routes.at(0)?.name !== SCREENS.SEARCH.BOTTOM_TAB) { - return { - adaptedState: state, - metainfo, - }; - } - - const routes = [...state.routes]; - const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(state); - if (matchingCentralPaneRoute) { - routes.push(matchingCentralPaneRoute); - } else { - // If there is no matching central pane, we want to add the default one. - metainfo.isCentralPaneAndBottomTabMandatory = false; - routes.push({name: SCREENS.REPORT}); - } - - return { - adaptedState: getRoutesWithIndex(routes), - metainfo, - }; - } - - return { - adaptedState: state, - metainfo, - }; -} - -const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldReplacePathInNestedState = true) => { - const normalizedPath = !path.startsWith('/') ? `/${path}` : path; - const pathWithoutPolicyID = getPathWithoutPolicyID(normalizedPath); - const isAnonymous = isAnonymousUser(); - - // Anonymous users don't have access to workspaces - const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path); - - const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; - if (shouldReplacePathInNestedState) { - replacePathInNestedState(state, normalizedPath); - } - if (state === undefined) { - throw new Error('Unable to parse path'); - } - - // On SCREENS.SEARCH.CENTRAL_PANE policyID is stored differently inside search query ("q" param), so we're handling this case - const focusedRoute = findFocusedRoute(state); - const policyIDFromQuery = extractPolicyIDFromQuery(focusedRoute); - - return getAdaptedState(state, policyID ?? policyIDFromQuery); -}; - -export default getAdaptedStateFromPath; -export type {Metainfo}; diff --git a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts deleted file mode 100644 index 7b213fdfeb6e..000000000000 --- a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts +++ /dev/null @@ -1,33 +0,0 @@ -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import type {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; -import {CENTRAL_PANE_TO_TAB_MAPPING} from './TAB_TO_CENTRAL_PANE_MAPPING'; - -// Get the route that matches the topmost central pane route in the navigation stack. e.g REPORT -> HOME -function getMatchingBottomTabRouteForState(state: State, policyID?: string): NavigationPartialRoute { - const paramsWithPolicyID = policyID ? {policyID} : undefined; - const defaultRoute = {name: SCREENS.HOME, params: paramsWithPolicyID}; - const isFullScreenNavigatorOpened = state.routes.some((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); - - if (isFullScreenNavigatorOpened) { - return {name: SCREENS.SETTINGS.ROOT, params: paramsWithPolicyID}; - } - - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state); - - if (topmostCentralPaneRoute === undefined) { - return defaultRoute; - } - - const tabName = CENTRAL_PANE_TO_TAB_MAPPING[topmostCentralPaneRoute.name]; - - if (tabName === SCREENS.SEARCH.BOTTOM_TAB) { - const topmostCentralPaneRouteParams = {...topmostCentralPaneRoute.params} as Record; - return {name: tabName, params: topmostCentralPaneRouteParams}; - } - - return {name: tabName, params: paramsWithPolicyID}; -} - -export default getMatchingBottomTabRouteForState; diff --git a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts deleted file mode 100644 index cec00f705127..000000000000 --- a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts +++ /dev/null @@ -1,74 +0,0 @@ -import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; -import type {AuthScreensParamList, CentralPaneName, NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; -import TAB_TO_CENTRAL_PANE_MAPPING from './TAB_TO_CENTRAL_PANE_MAPPING'; - -/** - * @param state - react-navigation state - */ -const getTopMostReportIDFromRHP = (state: State): string => { - if (!state) { - return ''; - } - - const topmostRightPane = state.routes.filter((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR).at(-1); - - if (topmostRightPane?.state) { - return getTopMostReportIDFromRHP(topmostRightPane.state); - } - - const topmostRoute = state.routes.at(-1); - - if (topmostRoute?.state) { - return getTopMostReportIDFromRHP(topmostRoute.state); - } - - if (topmostRoute?.params && 'reportID' in topmostRoute.params && typeof topmostRoute.params.reportID === 'string') { - return topmostRoute.params.reportID; - } - - return ''; -}; - -// Get already opened settings screen within the policy -function getAlreadyOpenedSettingsScreen(rootState?: State): keyof AuthScreensParamList | undefined { - if (!rootState) { - return undefined; - } - - // If one of the screen from TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.SETTINGS.ROOT] is now in the navigation state, we can decide which screen we should display. - // A screen from the navigation state can be pushed to the navigation state again only if it has a matching policyID with the currently selected workspace. - // Otherwise, when we switch the workspace, we want to display the initial screen in the settings tab. - const alreadyOpenedSettingsScreen = rootState.routes.filter((item) => TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.SETTINGS.ROOT].includes(item.name as CentralPaneName)).at(-1); - - return alreadyOpenedSettingsScreen?.name as keyof AuthScreensParamList; -} - -// Get matching central pane route for bottom tab navigator. e.g HOME -> REPORT -function getMatchingCentralPaneRouteForState(state: State, rootState?: State): NavigationPartialRoute | undefined { - const topmostBottomTabRoute = getTopmostBottomTabRoute(state); - - if (!topmostBottomTabRoute) { - return; - } - - const centralPaneName = TAB_TO_CENTRAL_PANE_MAPPING[topmostBottomTabRoute.name].at(0); - if (!centralPaneName) { - return; - } - - if (topmostBottomTabRoute.name === SCREENS.SETTINGS.ROOT) { - // When we go back to the settings tab without switching the workspace id, we want to return to the previously opened screen - const screen = getAlreadyOpenedSettingsScreen(rootState) ?? centralPaneName; - return {name: screen as CentralPaneName, params: topmostBottomTabRoute.params}; - } - - if (topmostBottomTabRoute.name === SCREENS.HOME) { - return {name: centralPaneName, params: {reportID: getTopMostReportIDFromRHP(state)}}; - } - - return {name: centralPaneName}; -} - -export default getMatchingCentralPaneRouteForState; diff --git a/src/libs/Navigation/linkingConfig/index.ts b/src/libs/Navigation/linkingConfig/index.ts index 1f556aa67809..dcdd14241cff 100644 --- a/src/libs/Navigation/linkingConfig/index.ts +++ b/src/libs/Navigation/linkingConfig/index.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type {LinkingOptions} from '@react-navigation/native'; +import {customGetPathFromState} from '@libs/Navigation/helpers'; +import getAdaptedStateFromPath from '@libs/Navigation/helpers/getAdaptedStateFromPath'; import type {RootStackParamList} from '@navigation/types'; import config from './config'; -import customGetPathFromState from './customGetPathFromState'; -import getAdaptedStateFromPath from './getAdaptedStateFromPath'; import prefixes from './prefixes'; -import subscribe from './subscribe'; const linkingConfig: LinkingOptions = { getStateFromPath: (...args) => { @@ -14,7 +13,6 @@ const linkingConfig: LinkingOptions = { // ResultState | undefined is the type this function expect. return adaptedState; }, - subscribe, getPathFromState: customGetPathFromState, prefixes, config, diff --git a/src/libs/Navigation/linkingConfig/subscribe/index.native.ts b/src/libs/Navigation/linkingConfig/subscribe/index.native.ts deleted file mode 100644 index 46720e9884e9..000000000000 --- a/src/libs/Navigation/linkingConfig/subscribe/index.native.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type {LinkingOptions} from '@react-navigation/native'; -import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; -import extractPathFromURL from '@react-navigation/native/src/extractPathFromURL'; -import {Linking} from 'react-native'; -import Navigation from '@libs/Navigation/Navigation'; -import config from '@navigation/linkingConfig/config'; -import prefixes from '@navigation/linkingConfig/prefixes'; -import type {RootStackParamList} from '@navigation/types'; -import type {Route} from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; - -// This field in linkingConfig is supported on native only. -const subscribe: LinkingOptions['subscribe'] = (listener) => { - // We need to override the default behaviour for the deep link to search screen. - // Even on mobile narrow layout, this screen need to push two screens on the stack to work (bottom tab and central pane). - // That's why we are going to handle it with our navigate function instead the default react-navigation one. - const linkingSubscription = Linking.addEventListener('url', ({url}) => { - const path = extractPathFromURL(prefixes, url); - - if (path) { - const stateFromPath = getStateFromPath(path, config); - if (stateFromPath) { - const focusedRoute = findFocusedRoute(stateFromPath); - if (focusedRoute && focusedRoute.name === SCREENS.SEARCH.CENTRAL_PANE) { - Navigation.navigate(path as Route); - return; - } - } - } - - listener(url); - }); - return () => { - // Clean up the event listeners - linkingSubscription.remove(); - }; -}; - -export default subscribe; diff --git a/src/libs/Navigation/linkingConfig/subscribe/index.ts b/src/libs/Navigation/linkingConfig/subscribe/index.ts deleted file mode 100644 index 74ef4133cb55..000000000000 --- a/src/libs/Navigation/linkingConfig/subscribe/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {LinkingOptions} from '@react-navigation/native'; -import type {RootStackParamList} from '@libs/Navigation/types'; - -// This field in linkingConfig is supported on native only. -const subscribe: LinkingOptions['subscribe'] = undefined; - -export default subscribe; diff --git a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts deleted file mode 100644 index d31c3693d495..000000000000 --- a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {findFocusedRoute, StackActions} from '@react-navigation/native'; -import {BackHandler, NativeModules} from 'react-native'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import getTopmostCentralPaneRoute from '@navigation/getTopmostCentralPaneRoute'; -import navigationRef from '@navigation/navigationRef'; -import type {BottomTabNavigatorParamList, RootStackParamList, State} from '@navigation/types'; -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; - -type SearchPageProps = PlatformStackScreenProps; - -// We need to do some custom handling for the back button on Android for actions related to the search page. -function setupCustomAndroidBackHandler() { - const onBackPress = () => { - const rootState = navigationRef.getRootState(); - const bottomTabRoute = rootState?.routes?.find((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); - const bottomTabRoutes = bottomTabRoute?.state?.routes; - const focusedRoute = findFocusedRoute(rootState); - - // Shouldn't happen but for type safety. - if (!bottomTabRoutes) { - return false; - } - - const isLastScreenOnStack = bottomTabRoutes.length === 1 && rootState?.routes?.length === 1; - - if (NativeModules.HybridAppModule && isLastScreenOnStack) { - NativeModules.HybridAppModule.exitApp(); - } - - // Handle back press on the search page. - // We need to pop two screens, from the central pane and from the bottom tab. - if (bottomTabRoutes[bottomTabRoutes.length - 1].name === SCREENS.SEARCH.BOTTOM_TAB && focusedRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { - navigationRef.dispatch({...StackActions.pop(), target: bottomTabRoute?.state?.key}); - navigationRef.dispatch({...StackActions.pop()}); - - const centralPaneRouteAfterPop = getTopmostCentralPaneRoute({routes: [rootState?.routes?.at(-2)]} as State); - const bottomTabRouteAfterPop = bottomTabRoutes.at(-2); - - // It's possible that central pane search is desynchronized with the bottom tab search. - // e.g. opening a tab different from search will wipe out central pane screens. - // In that case we have to push the proper one. - if ( - bottomTabRouteAfterPop && - bottomTabRouteAfterPop.name === SCREENS.SEARCH.BOTTOM_TAB && - (!centralPaneRouteAfterPop || centralPaneRouteAfterPop.name !== SCREENS.SEARCH.CENTRAL_PANE) - ) { - const searchParams = bottomTabRoutes[bottomTabRoutes.length - 2].params as SearchPageProps['route']['params']; - navigationRef.dispatch({...StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, searchParams)}); - } - - return true; - } - - // Handle back press to go back to the search page. - // It's possible that central pane search is desynchronized with the bottom tab search. - // e.g. opening a tab different from search will wipe out central pane screens. - // In that case we have to push the proper one. - if (bottomTabRoutes && bottomTabRoutes?.length >= 2 && bottomTabRoutes[bottomTabRoutes.length - 2].name === SCREENS.SEARCH.BOTTOM_TAB && rootState?.routes?.length === 1) { - const searchParams = bottomTabRoutes[bottomTabRoutes.length - 2].params as SearchPageProps['route']['params']; - navigationRef.dispatch({...StackActions.push(SCREENS.SEARCH.CENTRAL_PANE, searchParams)}); - navigationRef.dispatch({...StackActions.pop(), target: bottomTabRoute?.state?.key}); - return true; - } - - // Handle all other cases with default handler. - return false; - }; - - BackHandler.addEventListener('hardwareBackPress', onBackPress); -} - -export default setupCustomAndroidBackHandler; diff --git a/src/libs/Navigation/shouldSetScreenBlurred/index.native.tsx b/src/libs/Navigation/shouldSetScreenBlurred/index.native.tsx deleted file mode 100644 index 4043fddb7372..000000000000 --- a/src/libs/Navigation/shouldSetScreenBlurred/index.native.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @param navigationIndex - * - * Decides whether to set screen to blurred state. - * - * If the screen is more than 1 screen away from the current screen, freeze it, - * we don't want to freeze the screen if it's the previous screen because the freeze placeholder - * would be visible at the beginning of the back animation then - */ -const shouldSetScreenBlurred = (navigationIndex: number) => navigationIndex > 1; - -export default shouldSetScreenBlurred; diff --git a/src/libs/Navigation/shouldSetScreenBlurred/index.tsx b/src/libs/Navigation/shouldSetScreenBlurred/index.tsx deleted file mode 100644 index 14b45921bdb2..000000000000 --- a/src/libs/Navigation/shouldSetScreenBlurred/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @param navigationIndex - * - * Decides whether to set screen to blurred state. - * - * Allow freezing the first screen and more in the stack only on - * web and desktop platforms. The reason is that in the case of - * LHN, we have FlashList rendering in the back while we are on - * Settings screen. - */ -const shouldSetScreenBlurred = (navigationIndex: number) => navigationIndex >= 1; - -export default shouldSetScreenBlurred; diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts deleted file mode 100644 index 144a56c7e522..000000000000 --- a/src/libs/Navigation/switchPolicyID.ts +++ /dev/null @@ -1,157 +0,0 @@ -import {CommonActions, getActionFromState} from '@react-navigation/core'; -import type {NavigationAction, NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; -import {getPathFromState} from '@react-navigation/native'; -import type {Writable} from 'type-fest'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -import {isCentralPaneName} from '@libs/NavigationUtils'; -import * as SearchQueryUtils from '@libs/SearchQueryUtils'; -import CONST from '@src/CONST'; -import type {Route} from '@src/ROUTES'; -import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; -import getStateFromPath from './getStateFromPath'; -import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; -import linkingConfig from './linkingConfig'; -import type {NavigationRoot, RootStackParamList, StackNavigationAction, State, SwitchPolicyIDParams} from './types'; - -type ActionPayloadParams = { - screen?: string; - params?: unknown; - path?: string; -}; - -type CentralPaneRouteParams = Record & {policyID?: string; q?: string; reportID?: string}; - -function checkIfActionPayloadNameIsEqual(action: Writable, screenName: string) { - return action?.payload && 'name' in action.payload && action?.payload?.name === screenName; -} - -function getActionForBottomTabNavigator(action: StackNavigationAction, state: NavigationState, policyID?: string): Writable | undefined { - const bottomTabNavigatorRoute = state.routes.at(0); - - if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.state === undefined || !action || action.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { - return; - } - - let name: string | undefined; - let params: Record; - if (isCentralPaneName(action.payload.name)) { - name = action.payload.name; - params = action.payload.params as Record; - } else { - const actionPayloadParams = action.payload.params as ActionPayloadParams; - name = actionPayloadParams.screen; - params = actionPayloadParams?.params as Record; - } - - if (name === SCREENS.SEARCH.CENTRAL_PANE) { - name = SCREENS.SEARCH.BOTTOM_TAB; - } else if (!params) { - params = {policyID}; - } else { - params.policyID = policyID; - } - - // If the last route in the BottomTabNavigator is already a 'Home' route, we want to change the params rather than pushing a new 'Home' route, - // so that the screen does not get re-mounted. This would cause an empty screen/white flash when navigating back from the workspace switcher. - const homeRoute = bottomTabNavigatorRoute.state.routes.at(-1); - if (homeRoute && homeRoute.name === SCREENS.HOME) { - return { - ...CommonActions.setParams(params), - source: homeRoute?.key, - }; - } - - // If there is no 'Home' route in the BottomTabNavigator or if we are updating a different navigator, we want to push a new route. - return { - type: CONST.NAVIGATION.ACTION_TYPE.PUSH, - payload: { - name, - params, - }, - target: bottomTabNavigatorRoute.state.key, - }; -} - -export default function switchPolicyID(navigation: NavigationContainerRef | null, {policyID, route}: SwitchPolicyIDParams) { - if (!navigation) { - throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); - } - let root: NavigationRoot = navigation; - let current: NavigationRoot | undefined; - - // Traverse up to get the root navigation - // eslint-disable-next-line no-cond-assign - while ((current = root.getParent())) { - root = current; - } - - const rootState = navigation.getRootState() as NavigationState; - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState); - let newPath = route ?? getPathFromState({routes: rootState.routes} as State, linkingConfig.config); - - // Currently, the search page displayed in the bottom tab has the same URL as the page in the central pane, so we need to redirect to the correct search route. - // Here's the configuration: src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx - const isOpeningSearchFromBottomTab = !route && topmostCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE; - if (isOpeningSearchFromBottomTab) { - newPath = ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()}); - } - const stateFromPath = getStateFromPath(newPath as Route) as PartialState>; - const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config); - - const actionForBottomTabNavigator = getActionForBottomTabNavigator(action, rootState, policyID); - - if (!actionForBottomTabNavigator) { - return; - } - - root.dispatch(actionForBottomTabNavigator); - - // If path is passed to this method, it means that screen is pushed to the Central Pane from another place in code - if (route) { - return; - } - - // The correct route for SearchPage is located in the CentralPane - const shouldAddToCentralPane = !getIsNarrowLayout() || isOpeningSearchFromBottomTab; - - // If the layout is wide we need to push matching central pane route to the stack. - if (shouldAddToCentralPane) { - const params: CentralPaneRouteParams = {...topmostCentralPaneRoute?.params}; - - if (isOpeningSearchFromBottomTab && params.q) { - delete params.policyID; - const queryJSON = SearchQueryUtils.buildSearchQueryJSON(params.q); - - if (policyID) { - if (queryJSON) { - queryJSON.policyID = policyID; - params.q = SearchQueryUtils.buildSearchQueryString(queryJSON); - } - } else if (queryJSON) { - delete queryJSON.policyID; - params.q = SearchQueryUtils.buildSearchQueryString(queryJSON); - } - } - - // If the user is on the home page and changes the current workspace, then should be displayed a report from the selected workspace. - // To achieve that, it's necessary to navigate without the reportID param. - if (checkIfActionPayloadNameIsEqual(actionForBottomTabNavigator, SCREENS.HOME)) { - delete params.reportID; - } - - root.dispatch({ - type: CONST.NAVIGATION.ACTION_TYPE.PUSH, - payload: { - name: topmostCentralPaneRoute?.name, - params, - }, - }); - } else { - // If the layout is small we need to pop everything from the central pane so the bottom tab navigator is visible. - root.dispatch({ - type: 'POP_TO_TOP', - target: rootState.key, - }); - } -} diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 86948ea7f099..6662633e8336 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -22,6 +22,7 @@ import type SCREENS from '@src/SCREENS'; import type EXIT_SURVEY_REASON_FORM_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm'; import type {CompanyCardFeed} from '@src/types/onyx'; import type {ConnectionName, SageIntacctMappingName} from '@src/types/onyx/Policy'; +import type SIDEBAR_TO_SPLIT from './linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT'; type NavigationRef = NavigationContainerRefWithCurrent; @@ -52,29 +53,16 @@ type NavigationPartialRoute = PartialRoute = NavigationState | PartialState>; -type CentralPaneScreensParamList = { - [SCREENS.REPORT]: { - reportActionID: string; - reportID: string; - openOnAdminRoom?: boolean; - referrer?: string; - }; - [SCREENS.SETTINGS.PROFILE.ROOT]: undefined; - [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; - [SCREENS.SETTINGS.SECURITY]: undefined; - [SCREENS.SETTINGS.WALLET.ROOT]: undefined; - [SCREENS.SETTINGS.ABOUT]: undefined; - [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; - [SCREENS.SETTINGS.WORKSPACES]: undefined; +type SplitNavigatorSidebarScreen = keyof typeof SIDEBAR_TO_SPLIT; - [SCREENS.SEARCH.CENTRAL_PANE]: { - q: SearchQueryString; - name?: string; - }; - [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; - [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; +type SplitNavigatorParamListType = { + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: SettingsSplitNavigatorParamList; + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: ReportsSplitNavigatorParamList; + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: WorkspaceSplitNavigatorParamList; }; +type SplitNavigatorBySidebar = (typeof SIDEBAR_TO_SPLIT)[T]; + type BackToParams = { backTo?: Routes; }; @@ -111,17 +99,16 @@ type SettingsNavigatorParamList = { backTo?: Routes; forwardTo?: Routes; }; - [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; + // [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: undefined; [SCREENS.SETTINGS.PREFERENCES.LANGUAGE]: undefined; [SCREENS.SETTINGS.PREFERENCES.THEME]: undefined; [SCREENS.SETTINGS.CLOSE]: undefined; - [SCREENS.SETTINGS.SECURITY]: undefined; - [SCREENS.SETTINGS.ABOUT]: undefined; - [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; + // [SCREENS.SETTINGS.SECURITY]: undefined; + // [SCREENS.SETTINGS.ABOUT]: undefined; [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: undefined; - [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; + // [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; [SCREENS.SETTINGS.CONSOLE]: { backTo: Routes; }; @@ -1409,7 +1396,30 @@ type TravelNavigatorParamList = { [SCREENS.TRAVEL.MY_TRIPS]: undefined; }; -type FullScreenNavigatorParamList = { +type ReportsSplitNavigatorParamList = { + [SCREENS.HOME]: {policyID?: string}; + [SCREENS.REPORT]: { + reportActionID: string; + reportID: string; + openOnAdminRoom?: boolean; + referrer?: string; + }; +}; + +type SettingsSplitNavigatorParamList = { + [SCREENS.SETTINGS.ROOT]: {policyID?: string}; + [SCREENS.SETTINGS.WORKSPACES]: undefined; + [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; + [SCREENS.SETTINGS.SECURITY]: undefined; + [SCREENS.SETTINGS.PROFILE.ROOT]: undefined; + [SCREENS.SETTINGS.WALLET.ROOT]: undefined; + [SCREENS.SETTINGS.ABOUT]: undefined; + [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; + [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; +}; + +type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.INITIAL]: { policyID: string; backTo?: string; @@ -1540,14 +1550,8 @@ type MigratedUserModalNavigatorParamList = { [SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT]: undefined; }; -type BottomTabNavigatorParamList = { - [SCREENS.HOME]: {policyID?: string}; - [SCREENS.SEARCH.BOTTOM_TAB]: undefined; - [SCREENS.SETTINGS.ROOT]: {policyID?: string}; -}; - type SharedScreensParamList = { - [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: NavigatorScreenParams; [SCREENS.TRANSITION_BETWEEN_APPS]: { email?: string; accountID?: number; @@ -1577,52 +1581,57 @@ type PublicScreensParamList = SharedScreensParamList & { [SCREENS.CONNECTION_COMPLETE]: undefined; }; -type AuthScreensParamList = CentralPaneScreensParamList & - SharedScreensParamList & { - [SCREENS.CONCIERGE]: undefined; - [SCREENS.TRACK_EXPENSE]: undefined; - [SCREENS.SUBMIT_EXPENSE]: undefined; - [SCREENS.ATTACHMENTS]: { - reportID: string; - source: string; - type: ValueOf; - accountID: string; - isAuthTokenRequired?: string; - fileName?: string; - attachmentLink?: string; - }; - [SCREENS.PROFILE_AVATAR]: { - accountID: string; - }; - [SCREENS.WORKSPACE_AVATAR]: { - policyID: string; - }; - [SCREENS.WORKSPACE_JOIN_USER]: { - policyID: string; - email: string; - }; - [SCREENS.REPORT_AVATAR]: { - reportID: string; - policyID?: string; - }; - [SCREENS.NOT_FOUND]: undefined; - [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.FULL_SCREEN_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.EXPLANATION_MODAL_NAVIGATOR]: NavigatorScreenParams; - [NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR]: NavigatorScreenParams; - [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; - [SCREENS.TRANSACTION_RECEIPT]: { - reportID: string; - transactionID: string; - readonly?: string; - isFromReviewDuplicates?: string; - }; - [SCREENS.CONNECTION_COMPLETE]: undefined; +type AuthScreensParamList = SharedScreensParamList & { + [SCREENS.CONCIERGE]: undefined; + [SCREENS.TRACK_EXPENSE]: undefined; + [SCREENS.SUBMIT_EXPENSE]: undefined; + [SCREENS.ATTACHMENTS]: { + reportID: string; + source: string; + type: ValueOf; + accountID: string; + isAuthTokenRequired?: string; + fileName?: string; + attachmentLink?: string; + }; + [SCREENS.PROFILE_AVATAR]: { + accountID: string; + }; + [SCREENS.WORKSPACE_AVATAR]: { + policyID: string; + }; + [SCREENS.WORKSPACE_JOIN_USER]: { + policyID: string; + email: string; + }; + [SCREENS.REPORT_AVATAR]: { + reportID: string; + policyID?: string; + }; + [SCREENS.NOT_FOUND]: undefined; + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: NavigatorScreenParams & {policyID?: string}; + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: NavigatorScreenParams & {policyID?: string}; + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.EXPLANATION_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR]: NavigatorScreenParams; + [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; + [SCREENS.TRANSACTION_RECEIPT]: { + reportID: string; + transactionID: string; + readonly?: string; + isFromReviewDuplicates?: string; + }; + [SCREENS.CONNECTION_COMPLETE]: undefined; + [SCREENS.SEARCH.CENTRAL_PANE]: { + q: SearchQueryString; + name?: string; }; +}; type SearchReportParamList = { [SCREENS.SEARCH.REPORT_RHP]: { @@ -1699,37 +1708,39 @@ type DebugParamList = { type RootStackParamList = PublicScreensParamList & AuthScreensParamList & LeftModalNavigatorParamList; -type BottomTabName = keyof BottomTabNavigatorParamList; +type WorkspaceScreenName = keyof WorkspaceSplitNavigatorParamList; -type FullScreenName = keyof FullScreenNavigatorParamList; +type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; -type CentralPaneName = keyof CentralPaneScreensParamList; +type SplitNavigatorName = keyof SplitNavigatorParamListType; -type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; +type SplitNavigatorScreenName = keyof (WorkspaceSplitNavigatorParamList & SettingsSplitNavigatorParamList & ReportsSplitNavigatorParamList); -type SwitchPolicyIDParams = { - policyID?: string; - route?: Routes; - isPolicyAdmin?: boolean; -}; +type FullScreenName = SplitNavigatorName | typeof SCREENS.SEARCH.CENTRAL_PANE; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace ReactNavigation { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-empty-interface + interface RootParamList extends RootStackParamList {} + } +} export type { AddPersonalBankAccountNavigatorParamList, AuthScreensParamList, - CentralPaneScreensParamList, - CentralPaneName, - BackToParams, BackToAndForwardToParms, - BottomTabName, - BottomTabNavigatorParamList, + BackToParams, + DebugParamList, DetailsNavigatorParamList, EditRequestNavigatorParamList, EnablePaymentsNavigatorParamList, ExplanationModalNavigatorParamList, + FeatureTrainingNavigatorParamList, FlagCommentNavigatorParamList, FullScreenName, - FullScreenNavigatorParamList, LeftModalNavigatorParamList, + MissingPersonalDetailsParamList, MoneyRequestNavigatorParamList, NavigationPartialRoute, NavigationRef, @@ -1737,8 +1748,8 @@ export type { NavigationStateRoute, NewChatNavigatorParamList, NewTaskNavigatorParamList, - OnboardingModalNavigatorParamList, OnboardingFlowName, + OnboardingModalNavigatorParamList, ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, ProfileNavigatorParamList, @@ -1748,28 +1759,33 @@ export type { ReportDescriptionNavigatorParamList, ReportDetailsNavigatorParamList, ReportSettingsNavigatorParamList, + ReportsSplitNavigatorParamList, + RestrictedActionParamList, RightModalNavigatorParamList, RoomMembersNavigatorParamList, RootStackParamList, + SearchAdvancedFiltersParamList, + SearchReportParamList, + SearchSavedSearchParamList, SettingsNavigatorParamList, + SettingsSplitNavigatorParamList, SignInNavigatorParamList, - FeatureTrainingNavigatorParamList, SplitDetailsNavigatorParamList, + SplitNavigatorBySidebar, + SplitNavigatorName, + SplitNavigatorParamListType, + SplitNavigatorScreenName, + SplitNavigatorSidebarScreen, StackNavigationAction, State, StateOrRoute, - SwitchPolicyIDParams, - TravelNavigatorParamList, TaskDetailsNavigatorParamList, TeachersUniteNavigatorParamList, + TransactionDuplicateNavigatorParamList, + TravelNavigatorParamList, WalletStatementNavigatorParamList, WelcomeVideoModalNavigatorParamList, - TransactionDuplicateNavigatorParamList, - SearchReportParamList, - SearchAdvancedFiltersParamList, - SearchSavedSearchParamList, - RestrictedActionParamList, - MissingPersonalDetailsParamList, - DebugParamList, + WorkspaceScreenName, + WorkspaceSplitNavigatorParamList, MigratedUserModalNavigatorParamList, }; diff --git a/src/libs/NavigationUtils.ts b/src/libs/NavigationUtils.ts deleted file mode 100644 index 2f967c2f9a5e..000000000000 --- a/src/libs/NavigationUtils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import cloneDeep from 'lodash/cloneDeep'; -import SCREENS from '@src/SCREENS'; -import getTopmostBottomTabRoute from './Navigation/getTopmostBottomTabRoute'; -import type {CentralPaneName, OnboardingFlowName, RootStackParamList, State} from './Navigation/types'; - -const CENTRAL_PANE_SCREEN_NAMES = new Set([ - SCREENS.SETTINGS.WORKSPACES, - SCREENS.SETTINGS.PREFERENCES.ROOT, - SCREENS.SETTINGS.SECURITY, - SCREENS.SETTINGS.PROFILE.ROOT, - SCREENS.SETTINGS.WALLET.ROOT, - SCREENS.SETTINGS.ABOUT, - SCREENS.SETTINGS.TROUBLESHOOT, - SCREENS.SETTINGS.SAVE_THE_WORLD, - SCREENS.SETTINGS.SUBSCRIPTION.ROOT, - SCREENS.SEARCH.CENTRAL_PANE, - SCREENS.REPORT, -]); - -const ONBOARDING_SCREEN_NAMES = new Set([ - SCREENS.ONBOARDING.PERSONAL_DETAILS, - SCREENS.ONBOARDING.PURPOSE, - SCREENS.ONBOARDING_MODAL.ONBOARDING, - SCREENS.ONBOARDING.EMPLOYEES, - SCREENS.ONBOARDING.ACCOUNTING, -]); - -const removePolicyIDParamFromState = (state: State) => { - const stateCopy = cloneDeep(state); - const bottomTabRoute = getTopmostBottomTabRoute(stateCopy); - if (bottomTabRoute?.params && 'policyID' in bottomTabRoute.params) { - delete bottomTabRoute.params.policyID; - } - return stateCopy; -}; - -function isCentralPaneName(screen: string | undefined): screen is CentralPaneName { - if (!screen) { - return false; - } - return CENTRAL_PANE_SCREEN_NAMES.has(screen as CentralPaneName); -} - -function isOnboardingFlowName(screen: string | undefined): screen is OnboardingFlowName { - if (!screen) { - return false; - } - - return ONBOARDING_SCREEN_NAMES.has(screen as OnboardingFlowName); -} - -export {isCentralPaneName, removePolicyIDParamFromState, isOnboardingFlowName}; diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts index 34d982469825..504ac353d1c4 100644 --- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts +++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts @@ -5,10 +5,7 @@ import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {ReportActionPushNotificationData} from '@libs/Notification/PushNotification/NotificationType'; -import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; -import * as ReportConnection from '@libs/ReportConnection'; -import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import {updateLastVisitedPath} from '@userActions/App'; import * as Modal from '@userActions/Modal'; @@ -16,7 +13,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PushNotification from '..'; let lastVisitedPath: string | undefined; @@ -77,9 +73,6 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati Log.info('[PushNotification] Navigating to report', false, {reportID, reportActionID}); const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; - const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); Navigation.isNavigationReady() .then(Navigation.waitForProtectedRoutes) @@ -97,10 +90,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati } Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - if (!reportBelongsToWorkspace) { - Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); + Navigation.navigateToReportWithPolicyCheck({reportID: String(reportID), policyIDToCheck: policyID}); updateLastVisitedPath(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); } catch (error) { let errorMessage = String(error); diff --git a/src/libs/ObjectUtils.ts b/src/libs/ObjectUtils.ts index 644fe1c7596e..9e5a4fc5d8d7 100644 --- a/src/libs/ObjectUtils.ts +++ b/src/libs/ObjectUtils.ts @@ -1,13 +1,20 @@ +const getDefinedKeys = (obj: Record): string[] => { + return Object.entries(obj) + .filter(([, value]) => value !== undefined) + .map(([key]) => key); +}; + const shallowCompare = (obj1?: Record, obj2?: Record): boolean => { if (!obj1 && !obj2) { return true; } if (obj1 && obj2) { - const keys1 = Object.keys(obj1); - const keys2 = Object.keys(obj2); + const keys1 = getDefinedKeys(obj1); + const keys2 = getDefinedKeys(obj2); return keys1.length === keys2.length && keys1.every((key) => obj1[key] === obj2[key]); } return false; }; -export default shallowCompare; +// eslint-disable-next-line import/prefer-default-export +export {shallowCompare}; diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 450a6d7f5481..2967a49512ea 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -2,8 +2,8 @@ import React from 'react'; import type {MutableRefObject} from 'react'; import type {TextInput} from 'react-native'; import SCREENS from '@src/SCREENS'; -import getTopmostRouteName from './Navigation/getTopmostRouteName'; -import isReportOpenInRHP from './Navigation/isReportOpenInRHP'; +import getTopmostRouteName from './Navigation/helpers/getTopmostRouteName'; +import isReportOpenInRHP from './Navigation/helpers/isReportOpenInRHP'; import navigationRef from './Navigation/navigationRef'; type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a304ee800131..8bc05aacb67f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -74,8 +74,9 @@ import * as Localize from './Localize'; import Log from './Log'; import {isEmailPublicDomain} from './LoginUtils'; import ModifiedExpenseMessage from './ModifiedExpenseMessage'; +import {isFullScreenName} from './Navigation/helpers/isNavigatorName'; import linkingConfig from './Navigation/linkingConfig'; -import Navigation from './Navigation/Navigation'; +import Navigation, {navigationRef} from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; import Parser from './Parser'; import Permissions from './Permissions'; @@ -4214,8 +4215,10 @@ function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP if (!backRoute) { return; } - const topmostCentralPaneRoute = Navigation.getTopMostCentralPaneRouteFromRootState(); - if (topmostCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { + + const rootState = navigationRef.current?.getRootState(); + const lastFullScreenRoute = rootState?.routes.findLast((route) => isFullScreenName(route.name)); + if (lastFullScreenRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { Navigation.dismissModal(); return; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 6c804ba8d256..f7924e6502bc 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -39,7 +39,7 @@ import GoogleTagManager from '@libs/GoogleTagManager'; import * as IOUUtils from '@libs/IOUUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; -import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import * as NextStepUtils from '@libs/NextStepUtils'; import {rand64} from '@libs/NumberUtils'; @@ -3805,7 +3805,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { } InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : activeReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : activeReportID); if (activeReportID) { Report.notifyNewAction(activeReportID, payeeAccountID); } @@ -3863,7 +3863,7 @@ function sendInvoice( API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - if (isSearchTopmostCentralPane()) { + if (isSearchTopmostFullScreenRoute()) { Navigation.dismissModal(); } else { Navigation.dismissModalWithReport(invoiceRoom); @@ -4069,10 +4069,10 @@ function trackExpense( } } InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : activeReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : activeReportID); if (action === CONST.IOU.ACTION.SHARE) { - if (isSearchTopmostCentralPane() && activeReportID) { + if (isSearchTopmostFullScreenRoute() && activeReportID) { Navigation.goBack(); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(activeReportID)); } @@ -4651,7 +4651,7 @@ function splitBill({ API.write(WRITE_COMMANDS.SPLIT_BILL, parameters, onyxData); InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : existingSplitChatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : existingSplitChatReportID); Report.notifyNewAction(splitData.chatReportID, currentUserAccountID); } @@ -4719,7 +4719,7 @@ function splitBillAndOpenReport({ API.write(WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, parameters, onyxData); InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : splitData.chatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : splitData.chatReportID); Report.notifyNewAction(splitData.chatReportID, currentUserAccountID); } @@ -5291,7 +5291,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA API.write(WRITE_COMMANDS.COMPLETE_SPLIT_BILL, parameters, {optimisticData, successData, failureData}); InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : chatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : chatReportID); Report.notifyNewAction(chatReportID, sessionAccountID); } @@ -5474,7 +5474,7 @@ function createDistanceRequest( API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); InteractionManager.runAfterInteractions(() => TransactionEdit.removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); const activeReportID = isMoneyRequestReport ? report?.reportID ?? '-1' : parameters.chatReportID; - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : activeReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : activeReportID); Report.notifyNewAction(activeReportID, userAccountID); } @@ -7035,7 +7035,7 @@ function sendMoneyElsewhere(report: OnyxEntry, amount: number, API.write(WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, params, {optimisticData, successData, failureData}); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : params.chatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : params.chatReportID); Report.notifyNewAction(params.chatReportID, managerID); } @@ -7048,7 +7048,7 @@ function sendMoneyWithWallet(report: OnyxEntry, amount: number API.write(WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, params, {optimisticData, successData, failureData}); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : params.chatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : params.chatReportID); Report.notifyNewAction(params.chatReportID, managerID); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 06e339ea1696..1b2fe78a728c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -67,8 +67,8 @@ import isPublicScreenRoute from '@libs/isPublicScreenRoute'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import {registerPaginationConfig} from '@libs/Middleware/Pagination'; +import {isOnboardingFlowName} from '@libs/Navigation/helpers'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import {isOnboardingFlowName} from '@libs/NavigationUtils'; import enhanceParameters from '@libs/Network/enhanceParameters'; import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; @@ -76,7 +76,6 @@ import Parser from '@libs/Parser'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; -import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; @@ -84,7 +83,6 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportConnection from '@libs/ReportConnection'; import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; import {getNavatticURL} from '@libs/TourUtils'; import {generateAccountID} from '@libs/UserUtils'; @@ -1137,10 +1135,9 @@ function navigateToAndOpenReport( openReport(report?.reportID ?? '', '', userLogins, newChat, undefined, undefined, undefined, avatarFile); if (shouldDismissModal) { Navigation.dismissModalWithReport(report); - } else { - Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '-1'), actionType); + return; } + Navigation.navigateToReportWithPolicyCheck({report}); } /** @@ -1475,7 +1472,7 @@ function handleReportChanged(report: OnyxEntry) { const currCallback = callback; callback = () => { currCallback(); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? '-1'), CONST.NAVIGATION.TYPE.UP); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? '-1'), CONST.NAVIGATION.ACTION_TYPE.REPLACE); }; // The report screen will listen to this event and transfer the draft comment to the existing report @@ -1644,7 +1641,8 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { // if we are linking to the report action, and we are deleting it, and it's not a deleted parent action, // we should navigate to its report in order to not show not found page if (Navigation.isActiveRoute(ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID)) && !isDeletedParentAction) { - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID), true); + // @TODO: Check if this method works the same as on the main branch + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); } } @@ -2452,10 +2450,13 @@ function deleteReport(reportID: string, shouldDeleteChildReports = false) { */ function navigateToConciergeChatAndDeleteReport(reportID: string, shouldPopToTop = false, shouldDeleteChildReports = false) { // Dismiss the current report screen and replace it with Concierge Chat + // @TODO: Check if this method works the same as on the main branch if (shouldPopToTop) { Navigation.setShouldPopAllStateOnUP(true); + Navigation.popToTop(); + } else { + Navigation.goBack(); } - Navigation.goBack(undefined, undefined, shouldPopToTop); navigateToConciergeChat(); InteractionManager.runAfterInteractions(() => { deleteReport(reportID, shouldDeleteChildReports); @@ -2639,12 +2640,7 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi const onClick = () => Modal.close(() => { const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); - const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; - const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID) : false; - if (!reportBelongsToWorkspace) { - Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - } - navigateFromNotification(reportID); + navigateFromNotification(reportID, policyID); }); if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE) { @@ -2858,7 +2854,7 @@ function openReportFromDeepLink(url: string) { return; } - Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(route as Route); }; // We need skip deeplinking if the user hasn't completed the guided setup flow. @@ -2892,7 +2888,7 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { Navigation.goBack(); } - navigateToConciergeChat(false, () => true, CONST.NAVIGATION.TYPE.UP); + navigateToConciergeChat(false, () => true, CONST.NAVIGATION.ACTION_TYPE.REPLACE); } } diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index e50eba7e596f..04b0378d8391 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -810,6 +810,7 @@ function cleanupSession() { PersistedRequests.clear(); NetworkConnection.clearReconnectionCallbacks(); SessionUtils.resetDidUserLogInDuringSession(); + // TODO: Check if this breaks something resetHomeRouteParams(); clearCache().then(() => { Log.info('Cleared all cache data', true, {}, true); diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts index 5a1b4fc0474a..3908f372b891 100644 --- a/src/libs/actions/Welcome/OnboardingFlow.ts +++ b/src/libs/actions/Welcome/OnboardingFlow.ts @@ -1,8 +1,8 @@ import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; import type {NavigationState, PartialState} from '@react-navigation/native'; import Onyx from 'react-native-onyx'; +import {getAdaptedStateFromPath} from '@libs/Navigation/helpers'; import linkingConfig from '@libs/Navigation/linkingConfig'; -import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath'; import {navigationRef} from '@libs/Navigation/Navigation'; import type {RootStackParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; diff --git a/src/libs/actions/navigateFromNotification/index.native.ts b/src/libs/actions/navigateFromNotification/index.native.ts index 488ec8ac74e8..9e0a98e6b1c4 100644 --- a/src/libs/actions/navigateFromNotification/index.native.ts +++ b/src/libs/actions/navigateFromNotification/index.native.ts @@ -1,8 +1,7 @@ import Navigation from '@libs/Navigation/Navigation'; -import ROUTES from '@src/ROUTES'; -const navigateFromNotification = (reportID: string) => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); +const navigateFromNotification = (reportID: string, policyIDToCheck?: string) => { + Navigation.navigateToReportWithPolicyCheck({reportID, policyIDToCheck}); }; export default navigateFromNotification; diff --git a/src/libs/actions/navigateFromNotification/index.ts b/src/libs/actions/navigateFromNotification/index.ts index f710a16a3e70..3a7d01947be8 100644 --- a/src/libs/actions/navigateFromNotification/index.ts +++ b/src/libs/actions/navigateFromNotification/index.ts @@ -1,9 +1,8 @@ import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -const navigateFromNotification = (reportID: string) => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, undefined, CONST.REFERRER.NOTIFICATION)); +const navigateFromNotification = (reportID: string, policyIDToCheck?: string) => { + Navigation.navigateToReportWithPolicyCheck({reportID, referrer: CONST.REFERRER.NOTIFICATION, policyIDToCheck}); }; export default navigateFromNotification; diff --git a/src/libs/freezeScreenWithLazyLoading.tsx b/src/libs/freezeScreenWithLazyLoading.tsx deleted file mode 100644 index eb3c8fa8bc63..000000000000 --- a/src/libs/freezeScreenWithLazyLoading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import memoize from './memoize'; -import FreezeWrapper from './Navigation/FreezeWrapper'; - -function FrozenScreen(WrappedComponent: React.ComponentType) { - return (props: TProps) => ( - - - - ); -} - -export default function freezeScreenWithLazyLoading(lazyComponent: () => React.ComponentType) { - return memoize( - () => { - const Component = lazyComponent(); - return FrozenScreen(Component); - }, - {monitoringName: 'freezeScreenWithLazyLoading'}, - ); -} diff --git a/src/libs/navigateAfterJoinRequest/index.desktop.ts b/src/libs/navigateAfterJoinRequest/index.desktop.ts index cf6d009291c8..461f5376e6c1 100644 --- a/src/libs/navigateAfterJoinRequest/index.desktop.ts +++ b/src/libs/navigateAfterJoinRequest/index.desktop.ts @@ -3,7 +3,8 @@ import Navigation from '@navigation/Navigation'; import ROUTES from '@src/ROUTES'; const navigateAfterJoinRequest = () => { - Navigation.goBack(undefined, false, true); + // @TODO: Check if this method works the same as on the main branch + Navigation.popToTop(); if (getIsSmallScreenWidth()) { Navigation.navigate(ROUTES.SETTINGS); } diff --git a/src/libs/navigateAfterJoinRequest/index.ts b/src/libs/navigateAfterJoinRequest/index.ts index 42e91d18c6ba..91b2fdade606 100644 --- a/src/libs/navigateAfterJoinRequest/index.ts +++ b/src/libs/navigateAfterJoinRequest/index.ts @@ -2,7 +2,8 @@ import Navigation from '@navigation/Navigation'; import ROUTES from '@src/ROUTES'; const navigateAfterJoinRequest = () => { - Navigation.goBack(undefined, false, true); + // @TODO: Check if this method works the same as on the main branch + Navigation.popToTop(); Navigation.navigate(ROUTES.SETTINGS); Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); }; diff --git a/src/libs/navigateAfterJoinRequest/index.web.ts b/src/libs/navigateAfterJoinRequest/index.web.ts index cf6d009291c8..461f5376e6c1 100644 --- a/src/libs/navigateAfterJoinRequest/index.web.ts +++ b/src/libs/navigateAfterJoinRequest/index.web.ts @@ -3,7 +3,8 @@ import Navigation from '@navigation/Navigation'; import ROUTES from '@src/ROUTES'; const navigateAfterJoinRequest = () => { - Navigation.goBack(undefined, false, true); + // @TODO: Check if this method works the same as on the main branch + Navigation.popToTop(); if (getIsSmallScreenWidth()) { Navigation.navigate(ROUTES.SETTINGS); } diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index d84927988b5c..494c70c907f4 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -1,7 +1,7 @@ import ROUTES from '@src/ROUTES'; import * as Report from './actions/Report'; +import {shouldOpenOnAdminRoom} from './Navigation/helpers'; import Navigation from './Navigation/Navigation'; -import shouldOpenOnAdminRoom from './Navigation/shouldOpenOnAdminRoom'; import * as ReportUtils from './ReportUtils'; const navigateAfterOnboarding = ( diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index d99db40d07ee..05e8606c9dd7 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -11,13 +11,14 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; -import Navigation from '@libs/Navigation/Navigation'; +import {isFullScreenName} from '@libs/Navigation/helpers'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import * as BankAccounts from '@userActions/BankAccounts'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; function AddPersonalBankAccountPage() { @@ -28,21 +29,22 @@ function AddPersonalBankAccountPage() { const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; - const topMostCentralPane = Navigation.getTopMostCentralPaneRouteFromRootState(); + const topmostFullScreenRoute = navigationRef.current?.getRootState().routes.findLast((route) => isFullScreenName(route.name)); + // @TODO: Verify if this method works correctly const goBack = useCallback(() => { - switch (topMostCentralPane?.name) { - case SCREENS.SETTINGS.WALLET.ROOT: - Navigation.goBack(ROUTES.SETTINGS_WALLET, true); + switch (topmostFullScreenRoute?.name) { + case NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR: + Navigation.goBack(ROUTES.SETTINGS_WALLET); break; - case SCREENS.REPORT: + case NAVIGATORS.REPORTS_SPLIT_NAVIGATOR: Navigation.closeRHPFlow(); break; default: Navigation.goBack(); break; } - }, [topMostCentralPane]); + }, [topmostFullScreenRoute]); const submitBankAccountForm = useCallback(() => { const bankAccounts = plaidData?.bankAccounts ?? []; diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 6b25681fa8e8..afa402959356 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -11,7 +11,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {EditRequestNavigatorParamList} from '@libs/Navigation/types'; @@ -69,14 +69,14 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { goBack(); } else { ReportActions.updateReportField(report.reportID, {...reportField, value: value === '' ? null : value}, reportField); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : report?.reportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : report?.reportID); } }; const handleReportFieldDelete = () => { ReportActions.deleteReportField(report.reportID, reportField); setIsDeleteModalVisible(false); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : report?.reportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : report?.reportID); }; const fieldValue = isReportFieldTitle ? report.reportName ?? '' : reportField.value ?? reportField.defaultValue; diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx index 55f19f8c35b9..a273b210efa9 100644 --- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx +++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx @@ -52,7 +52,7 @@ function AddBankAccount() { PaymentMethods.continueSetup(onSuccessFallbackRoute); return; } - Navigation.goBack(ROUTES.SETTINGS_WALLET, true); + Navigation.goBack(ROUTES.SETTINGS_WALLET); }; const handleBackButtonPress = () => { @@ -63,7 +63,7 @@ function AddBankAccount() { if (screenIndex === 0) { BankAccounts.clearPersonalBankAccount(); Wallet.updateCurrentStep(null); - Navigation.goBack(ROUTES.SETTINGS_WALLET, true); + Navigation.goBack(ROUTES.SETTINGS_WALLET); return; } prevScreen(); diff --git a/src/pages/EnablePayments/EnablePayments.tsx b/src/pages/EnablePayments/EnablePayments.tsx index b55141fec299..acbafafeea59 100644 --- a/src/pages/EnablePayments/EnablePayments.tsx +++ b/src/pages/EnablePayments/EnablePayments.tsx @@ -60,7 +60,7 @@ function EnablePaymentsPage() { > Navigation.goBack(ROUTES.SETTINGS_WALLET, true)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)} /> diff --git a/src/pages/EnablePayments/EnablePaymentsPage.tsx b/src/pages/EnablePayments/EnablePaymentsPage.tsx index 5fdfcca02660..64482b8e78c6 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.tsx +++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx @@ -40,7 +40,7 @@ function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (isPendingOnfidoResult || hasFailedOnfido) { - Navigation.navigate(ROUTES.SETTINGS_WALLET, CONST.NAVIGATION.TYPE.UP); + Navigation.navigate(ROUTES.SETTINGS_WALLET, CONST.NAVIGATION.ACTION_TYPE.REPLACE); return; } diff --git a/src/pages/GroupChatNameEditPage.tsx b/src/pages/GroupChatNameEditPage.tsx index 69d7f6c6f8af..d3d205a36783 100644 --- a/src/pages/GroupChatNameEditPage.tsx +++ b/src/pages/GroupChatNameEditPage.tsx @@ -69,7 +69,7 @@ function GroupChatNameEditPage({groupChatDraft, report}: GroupChatNameEditPagePr if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { Report.updateGroupChatName(reportID, values[INPUT_IDS.NEW_CHAT_NAME] ?? ''); } - Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); return; } if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 2271eee7c54e..9e8b566afbde 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -13,6 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; import type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; import CONST from '@src/CONST'; @@ -93,7 +94,15 @@ function PrivateNotesListPage({report, accountID: sessionAccountID}: PrivateNote Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo))} + onBackButtonPress={() => { + if (ReportUtils.isOneOnOneChat(report)) { + const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report); + Navigation.goBack(ROUTES.PROFILE.getRoute(participantAccountIDs.at(0), backTo)); + return; + } + + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo)); + }} onCloseButtonPress={() => Navigation.dismissModal()} /> ; function SearchPage({route}: SearchPageProps) { + const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); - const {q} = route.params; - const queryJSON = useMemo(() => SearchQueryUtils.buildSearchQueryJSON(q), [q]); + const {q, name} = route.params; + + const {queryJSON, policyID} = useMemo(() => { + const parsedQuery = SearchQueryUtils.buildSearchQueryJSON(q); + const extractedPolicyID = parsedQuery && SearchQueryUtils.getPolicyIDFromSearchQuery(parsedQuery); + + return {queryJSON: parsedQuery, policyID: extractedPolicyID}; + }, [q]); + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()})); + const {clearSelectedTransactions} = useSearchContext(); + + const isSearchNameModified = name === q; + const searchName = isSearchNameModified ? undefined : name; // On small screens this page is not displayed, the configuration is in the file: src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx // To avoid calling hooks in the Search component when this page isn't visible, we return null here. if (shouldUseNarrowLayout) { - return null; + return ( + + ); } return ( - + {!!queryJSON && ( - <> - - - - + + + {/* {!selectionMode?.isEnabled && queryJSON ? ( */} + {queryJSON ? ( + + + + + ) : ( + { + clearSelectedTransactions(); + turnOffMobileSelectionMode(); + }} + /> + )} + + + + + + + + )} - + ); } SearchPage.displayName = 'SearchPage'; +SearchPage.whyDidYouRender = true; export default SearchPage; diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index bfe378409af7..0f1f2ac33574 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -6,13 +6,13 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import SearchStatusBar from '@components/Search/SearchStatusBar'; -import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute'; +import type {SearchQueryJSON} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import BottomTabBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; import Navigation from '@libs/Navigation/Navigation'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import variables from '@styles/variables'; @@ -26,11 +26,17 @@ const TOO_CLOSE_TO_TOP_DISTANCE = 10; const TOO_CLOSE_TO_BOTTOM_DISTANCE = 10; const ANIMATION_DURATION_IN_MS = 300; -function SearchPageBottomTab() { +type SearchPageBottomTabProps = { + queryJSON?: SearchQueryJSON; + policyID?: string; + searchName?: string; +}; + +function SearchPageBottomTab({queryJSON, policyID, searchName}: SearchPageBottomTabProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); - const activeCentralPaneRoute = useActiveCentralPaneRoute(); + const styles = useThemeStyles(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); @@ -68,15 +74,6 @@ function SearchPageBottomTab() { [windowHeight, topBarOffset], ); - const searchParams = activeCentralPaneRoute?.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; - const parsedQuery = SearchQueryUtils.buildSearchQueryJSON(searchParams?.q); - const isSearchNameModified = searchParams?.name === searchParams?.q; - const searchName = isSearchNameModified ? undefined : searchParams?.name; - const policyIDFromSearchQuery = parsedQuery && SearchQueryUtils.getPolicyIDFromSearchQuery(parsedQuery); - const isActiveCentralPaneRoute = activeCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE; - const queryJSON = isActiveCentralPaneRoute ? parsedQuery : undefined; - const policyID = isActiveCentralPaneRoute ? policyIDFromSearchQuery : undefined; - const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()})); if (!queryJSON) { @@ -100,8 +97,8 @@ function SearchPageBottomTab() { return ( } > {!selectionMode?.isEnabled ? ( <> @@ -136,15 +133,12 @@ function SearchPageBottomTab() { ) : ( )} - {shouldUseNarrowLayout && ( - - )} + ); } diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx index 87ba17b6504d..e5312d763e04 100644 --- a/src/pages/WorkspaceSwitcherPage/index.tsx +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -11,12 +11,12 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import {sortWorkspacesBySelected} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; +import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -35,7 +35,7 @@ function WorkspaceSwitcherPage() { const {isOffline} = useNetwork(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {translate} = useLocalize(); - const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); + const {activeWorkspaceID} = useActiveWorkspace(); const isFocused = useIsFocused(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); @@ -84,13 +84,12 @@ function WorkspaceSwitcherPage() { } const newPolicyID = policyID === activeWorkspaceID ? undefined : policyID; - setActiveWorkspaceID(newPolicyID); Navigation.goBack(); - if (newPolicyID !== activeWorkspaceID) { - Navigation.navigateWithSwitchPolicyID({policyID: newPolicyID}); + if (policyID !== activeWorkspaceID) { + navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID, payload: {policyID: newPolicyID}}); } }, - [activeWorkspaceID, setActiveWorkspaceID, isFocused], + [activeWorkspaceID, isFocused], ); const usersWorkspaces = useMemo(() => { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 97582f75b7b1..b41efe93a406 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -39,7 +39,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import shouldFetchReport from '@libs/shouldFetchReport'; import * as ValidationUtils from '@libs/ValidationUtils'; -import type {AuthScreensParamList} from '@navigation/types'; +import type {ReportsSplitNavigatorParamList} from '@navigation/types'; import * as ComposerActions from '@userActions/Composer'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; @@ -56,7 +56,7 @@ import ReportFooter from './report/ReportFooter'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; -type ReportScreenNavigationProps = PlatformStackScreenProps; +type ReportScreenNavigationProps = PlatformStackScreenProps; type ReportScreenProps = CurrentReportIDContextValue & ReportScreenNavigationProps; @@ -112,7 +112,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const {isOffline} = useNetwork(); const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const {activeWorkspaceID} = useActiveWorkspace(); - const [modal] = useOnyx(ONYXKEYS.MODAL); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false}); const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, {initialValue: ''}); @@ -296,7 +295,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro Navigation.dismissModal(); return; } - Navigation.goBack(undefined, false, true); + // @TODO: Check if this method works the same as on the main branch + Navigation.popToTop(); }, [isInNarrowPaneModal]); let headerView = ( @@ -608,7 +608,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { Navigation.setShouldPopAllStateOnUP(true); - Navigation.goBack(undefined, false, true); + // @TODO: Check if this method works the same as on the main branch + Navigation.popToTop(); } if (prevReport?.parentReportID) { // Prevent navigation to the IOU/Expense Report if it is pending deletion. diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 9e151a56d329..777f75e0dfe3 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -3,10 +3,10 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; // eslint-disable-next-line lodash/import-scope import type {DebouncedFunc} from 'lodash'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; +import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import InvertedFlatList from '@components/InvertedFlatList'; import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -19,14 +19,14 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; -import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; -import type {AuthScreensParamList} from '@navigation/types'; +import type {ReportsSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; @@ -158,7 +158,7 @@ function ReportActionsList({ const {preferredLocale} = useLocalize(); const {isOffline, lastOfflineAt, lastOnlineAt} = useNetworkWithOfflineStatus(); - const route = useRoute>(); + const route = useRoute>(); const reportScrollManager = useReportScrollManager(); const userActiveSince = useRef(DateUtils.getDBTime()); const lastMessageTime = useRef(null); @@ -714,7 +714,7 @@ function ReportActionsList({ }, [isLoadingNewerReportActions, canShowHeader, hasLoadingNewerReportActionsError, retryLoadNewerChatsError]); const onStartReached = useCallback(() => { - if (!isSearchTopmostCentralPane()) { + if (!isSearchTopmostFullScreenRoute()) { loadNewerChats(false); return; } diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index ee7c929acc7d..95a8bc677ad7 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -11,7 +11,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types'; import * as NumberUtils from '@libs/NumberUtils'; import {generateNewRandomInt} from '@libs/NumberUtils'; import Performance from '@libs/Performance'; @@ -86,7 +86,7 @@ function ReportActionsView({ }: ReportActionsViewProps) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); - const route = useRoute>(); + const route = useRoute>(); const [session] = useOnyx(ONYXKEYS.SESSION); const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? -1}`, { selector: (reportActions: OnyxEntry) => diff --git a/src/pages/home/report/UserTypingEventListener.tsx b/src/pages/home/report/UserTypingEventListener.tsx index 73062902f63e..6609e48161b2 100644 --- a/src/pages/home/report/UserTypingEventListener.tsx +++ b/src/pages/home/report/UserTypingEventListener.tsx @@ -4,7 +4,7 @@ import {InteractionManager} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -19,7 +19,7 @@ function UserTypingEventListener({report}: UserTypingEventListenerProps) { const didSubscribeToReportTypingEvents = useRef(false); const reportID = report.reportID; const isFocused = useIsFocused(); - const route = useRoute>(); + const route = useRoute>(); useEffect( () => () => { diff --git a/src/pages/home/sidebar/BottomTabAvatar.tsx b/src/pages/home/sidebar/BottomTabAvatar.tsx index 32aa1d455b5e..28712438aea9 100644 --- a/src/pages/home/sidebar/BottomTabAvatar.tsx +++ b/src/pages/home/sidebar/BottomTabAvatar.tsx @@ -1,15 +1,21 @@ +import {useRoute} from '@react-navigation/native'; import React, {useCallback} from 'react'; import {useOnyx} from 'react-native-onyx'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; +import {getPreservedSplitNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import AvatarWithDelegateAvatar from './AvatarWithDelegateAvatar'; import AvatarWithOptionalStatus from './AvatarWithOptionalStatus'; import ProfileAvatarWithIndicator from './ProfileAvatarWithIndicator'; @@ -27,8 +33,10 @@ function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomT const {translate} = useLocalize(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const delegateEmail = account?.delegatedAccess?.delegate ?? ''; + const route = useRoute(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const emojiStatus = currentUserPersonalDetails?.status?.emojiCode ?? ''; + const {shouldUseNarrowLayout} = useResponsiveLayout(); const showSettingsPage = useCallback(() => { if (isCreateMenuOpen) { @@ -36,8 +44,51 @@ function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomT return; } - interceptAnonymousUser(() => Navigation.navigate(ROUTES.SETTINGS)); - }, [isCreateMenuOpen]); + if (route.name === SCREENS.SETTINGS.WORKSPACES && shouldUseNarrowLayout) { + Navigation.goBack(ROUTES.SETTINGS); + return; + } + + if (route.name === SCREENS.WORKSPACE.INITIAL) { + Navigation.goBack(ROUTES.SETTINGS); + if (shouldUseNarrowLayout) { + Navigation.navigate(ROUTES.SETTINGS, CONST.NAVIGATION.ACTION_TYPE.REPLACE); + } + return; + } + + interceptAnonymousUser(() => { + const rootState = navigationRef.getRootState(); + const lastSettingsOrWorkspaceNavigatorRoute = rootState.routes.findLast( + (rootRoute) => rootRoute.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR || rootRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, + ); + + // If there is a workspace navigator route, then we should open the workspace initial screen as it should be "remembered". + if (lastSettingsOrWorkspaceNavigatorRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + const state = lastSettingsOrWorkspaceNavigatorRoute.state ?? getPreservedSplitNavigatorState(lastSettingsOrWorkspaceNavigatorRoute.key); + const params = state?.routes.at(0)?.params as WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]; + // Screens of this navigator should always have policyID + if (params.policyID) { + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(params.policyID)); + } + return; + } + + // If there is settings workspace screen in the settings navigator, then we should open the settings workspaces as it should be "remembered". + if ( + lastSettingsOrWorkspaceNavigatorRoute && + lastSettingsOrWorkspaceNavigatorRoute.state && + lastSettingsOrWorkspaceNavigatorRoute.state.routes.at(-1)?.name === SCREENS.SETTINGS.WORKSPACES + ) { + Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); + return; + } + + // Otherwise we should simply open the settings navigator. + // This case also covers if there is no route to remember. + Navigation.navigate(ROUTES.SETTINGS); + }); + }, [isCreateMenuOpen, shouldUseNarrowLayout, route.name]); let children; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 890accbd26ac..5f049bda3a6f 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; -import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import {useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,7 +20,7 @@ type SidebarLinksDataProps = { function SidebarLinksData({insets}: SidebarLinksDataProps) { const isFocused = useIsFocused(); const styles = useThemeStyles(); - const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); + const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {initialValue: true}); const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {initialValue: CONST.PRIORITY_MODE.DEFAULT}); @@ -40,7 +40,6 @@ function SidebarLinksData({insets}: SidebarLinksDataProps) { // eslint-disable-next-line react-compiler/react-compiler currentReportIDRef.current = currentReportID; const isActiveReport = useCallback((reportID: string): boolean => currentReportIDRef.current === reportID, []); - return ( { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); @@ -46,8 +53,26 @@ function BaseSidebarScreen() { return; } + const topmostReport = navigationRef.getRootState()?.routes.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); + + if (!topmostReport) { + return; + } + + // Switching workspace to global should only be performed from the currently opened sidebar screen + const topmostReportState = topmostReport?.state ?? getPreservedSplitNavigatorState(topmostReport?.key); + const isCurrentSidebar = topmostReportState?.routes.some((route) => currentRoute.key === route.key); + + if (!isCurrentSidebar) { + return; + } + isSwitchingWorkspace.current = true; - Navigation.navigateWithSwitchPolicyID({policyID: undefined}); + navigationRef.current?.dispatch({ + target: navigationRef.current.getRootState().key, + payload: getInitialSplitNavigatorState({name: SCREENS.HOME}, {name: SCREENS.REPORT}), + type: CONST.NAVIGATION.ACTION_TYPE.REPLACE, + }); updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); @@ -55,11 +80,10 @@ function BaseSidebarScreen() { return ( } > {({insets}) => ( <> diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index d36003960fe4..c5376d1fd988 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -1,4 +1,4 @@ -import {useIsFocused as useIsFocusedOriginal, useNavigationState} from '@react-navigation/native'; +import {useIsFocused} from '@react-navigation/native'; import type {ImageContentFit} from 'expo-image'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; @@ -23,9 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation from '@libs/Navigation/Navigation'; -import type {CentralPaneName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; import {hasSeenTourSelector} from '@libs/onboardingSelectors'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -43,21 +41,11 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; -// On small screen we hide the search page from central pane to show the search bottom tab page with bottom tab bar. -// We need to take this in consideration when checking if the screen is focused. -const useIsFocused = () => { - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const isFocused = useIsFocusedOriginal(); - const topmostCentralPane = useNavigationState | undefined>(getTopmostCentralPaneRoute); - return isFocused || (topmostCentralPane?.name === SCREENS.SEARCH.CENTRAL_PANE && shouldUseNarrowLayout); -}; - type PolicySelector = Pick; type FloatingActionButtonAndPopoverProps = { diff --git a/src/pages/home/sidebar/SidebarScreen/index.tsx b/src/pages/home/sidebar/SidebarScreen/index.tsx index 625491674cd8..dc04c005b827 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.tsx +++ b/src/pages/home/sidebar/SidebarScreen/index.tsx @@ -1,13 +1,8 @@ import React from 'react'; -import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; function SidebarScreen() { - return ( - - - - ); + return ; } SidebarScreen.displayName = 'SidebarScreen'; diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 61bf7399889a..8989e9c8ebca 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -41,7 +41,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { } IOU.putOnHold(transactionID, values.comment, reportID, searchHash); - Navigation.navigate(backTo); + Navigation.goBack(backTo); }; const validate = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 989277bb5fc1..14a866654e28 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -183,7 +183,7 @@ function IOURequestStepConfirmation({ // back to the participants step if (!transaction?.participantsAutoAssigned && participantsAutoAssignedFromRoute !== 'true') { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, transaction?.reportID || reportID, undefined, action)); + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, transaction?.reportID || reportID, undefined, action), {compareParams: false}); return; } IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID, action); diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index a40b14eae4c9..77397737cb33 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,12 +1,14 @@ -import {useRoute} from '@react-navigation/native'; +import {useNavigationState, useRoute} from '@react-navigation/native'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, ScrollView as RNScrollView, ScrollViewProps, StyleProp, ViewStyle} from 'react-native'; import {NativeModules, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +// eslint-disable-next-line no-restricted-imports import AccountSwitcher from '@components/AccountSwitcher'; import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView'; +// eslint-disable-next-line no-restricted-imports import ConfirmModal from '@components/ConfirmModal'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; @@ -21,7 +23,6 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -31,6 +32,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import BottomTabBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; +import getTopmostRouteName from '@libs/Navigation/helpers/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -47,6 +50,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 SCREENS from '@src/SCREENS'; import type {Icon as TIcon} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -96,7 +100,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); const {translate} = useLocalize(); - const activeCentralPaneRoute = useActiveCentralPaneRoute(); + const activeRoute = useNavigationState(getTopmostRouteName); const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`); const {setInitialURL} = useContext(InitialURLContext); @@ -345,11 +349,8 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr ref={popoverAnchor} shouldBlockSelection={!!item.link} onSecondaryInteraction={item.link ? (event) => openPopover(item.link, event) : undefined} - focused={ - !!activeCentralPaneRoute && - !!item.routeName && - !!(activeCentralPaneRoute.name.toLowerCase().replaceAll('_', '') === item.routeName.toLowerCase().replaceAll('/', '')) - } + focused={!!activeRoute && !!item.routeName && !!(activeRoute.toLowerCase().replaceAll('_', '') === item.routeName.toLowerCase().replaceAll('/', ''))} + isPaneMenu iconRight={item.iconRight} shouldShowRightIcon={item.shouldShowRightIcon} shouldIconUseAutoWidthStyle @@ -359,7 +360,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr ); }, - [styles.pb4, styles.mh3, styles.sectionTitle, styles.sectionMenuItem, translate, userWallet?.currentBalance, isExecuting, singleExecution, activeCentralPaneRoute, waitForNavigate], + [styles.pb4, styles.mh3, styles.sectionTitle, styles.sectionMenuItem, translate, userWallet?.currentBalance, isExecuting, singleExecution, activeRoute, waitForNavigate], ); const accountMenuItems = useMemo(() => getMenuItemsSection(accountMenuItemsData), [accountMenuItemsData, getMenuItemsSection]); @@ -425,10 +426,9 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr return ( } shouldEnableKeyboardAvoidingView={false} > {headerContent} diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx index e0bf0e781e88..303ffa1c9967 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx @@ -18,7 +18,7 @@ import type SCREENS from '@src/SCREENS'; type CountrySelectionPageProps = PlatformStackScreenProps; -function CountrySelectionPage({route, navigation}: CountrySelectionPageProps) { +function CountrySelectionPage({route}: CountrySelectionPageProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); const currentCountry = route.params.country; @@ -44,19 +44,16 @@ function CountrySelectionPage({route, navigation}: CountrySelectionPageProps) { const selectCountry = useCallback( (option: Option) => { const backTo = route.params.backTo ?? ''; - // Check the navigation state and "backTo" parameter to decide navigation behavior - if (navigation.getState().routes.length === 1 && !backTo) { - // If there is only one route and "backTo" is empty, go back in navigation + + // Check the "backTo" parameter to decide navigation behavior + if (!backTo) { Navigation.goBack(); - } else if (!!backTo && navigation.getState().routes.length === 1) { - // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter - Navigation.goBack(appendParam(backTo, 'country', option.value)); } else { - // Otherwise, navigate to the specific route defined in "backTo" with a country parameter - Navigation.navigate(appendParam(backTo, 'country', option.value)); + // Set compareParams to false because we want to goUp to this particular screen and update params (country). + Navigation.goBack(appendParam(backTo, 'country', option.value), {compareParams: false}); } }, - [route, navigation], + [route], ); return ( diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx index 85bf9333588d..14a3248d2c80 100644 --- a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx @@ -1,6 +1,5 @@ -import {useNavigation, useRoute} from '@react-navigation/native'; +import {useRoute} from '@react-navigation/native'; import {CONST as COMMON_CONST} from 'expensify-common'; -import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -25,7 +24,6 @@ type RouteParams = { function StateSelectionPage() { const route = useRoute(); - const navigation = useNavigation(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -57,26 +55,15 @@ function StateSelectionPage() { (option: Option) => { const backTo = params?.backTo ?? ''; - // Determine navigation action based on "backTo" presence and route stack length. - if (navigation.getState()?.routes.length === 1) { - // If this is the only page in the navigation stack (examples include direct navigation to this page via URL or page reload). - if (isEmpty(backTo)) { - // No "backTo": default back navigation. - Navigation.goBack(); - } else { - // "backTo" provided: navigate back to "backTo" with state parameter. - Navigation.goBack(appendParam(backTo, 'state', option.value)); - } - } else if (!isEmpty(backTo)) { - // Most common case: Navigation stack has multiple routes and "backTo" is defined: navigate to "backTo" with state parameter. - Navigation.navigate(appendParam(backTo, 'state', option.value)); - } else { - // This is a fallback block and should never execute if StateSelector is correctly appending the "backTo" route. - // Navigation stack has multiple routes but no "backTo" defined: default back navigation. + // Check the "backTo" parameter to decide navigation behavior + if (!backTo) { Navigation.goBack(); + } else { + // Set compareParams to false because we want to goUp to this particular screen and update params (state). + Navigation.goBack(appendParam(backTo, 'state', option.value), {compareParams: false}); } }, - [navigation, params?.backTo], + [params?.backTo], ); return ( diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 153c5a65b3b4..e63e5f62f45f 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -57,7 +57,7 @@ function CardSection() { const requestRefund = useCallback(() => { User.requestRefund(); setIsRequestRefundModalVisible(false); - Navigation.resetToHome(); + Navigation.goBack(ROUTES.HOME); }, []); const viewPurchases = useCallback(() => { diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 3e2935e626cb..579a9412475d 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -9,22 +9,21 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; const goToGetPhysicalCardName = (domain: string) => { - Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain)); }; const goToGetPhysicalCardPhone = (domain: string) => { - Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain)); }; const goToGetPhysicalCardAddress = (domain: string) => { - Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS.getRoute(domain)); }; type GetPhysicalCardConfirmProps = PlatformStackScreenProps; diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx index f9fc3ff27ba6..7ffa55373308 100644 --- a/src/pages/settings/Wallet/VerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx @@ -63,7 +63,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { setIsValidateCodeActionModalVisible(false); if (navigateForwardTo) { - Navigation.navigate(navigateForwardTo, CONST.NAVIGATION.TYPE.UP); + Navigation.navigate(navigateForwardTo, CONST.NAVIGATION.ACTION_TYPE.REPLACE); } else { Navigation.goBack(backTo); } diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 9bda7f3972f9..39ac7b2f9451 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -17,7 +17,6 @@ import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import callOrReturn from '@src/types/utils/callOrReturn'; @@ -99,7 +98,7 @@ function PageNotFoundFallback({policyID, fullPageNotFoundViewProps, isFeatureEna shouldForceFullScreen={shouldShowFullScreenFallback} onBackButtonPress={() => { if (isPolicyNotAccessible) { - Navigation.dismissModal(); + Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); return; } Navigation.goBack(policyID && !isMoneyRequest ? ROUTES.WORKSPACE_PROFILE.getRoute(policyID) : undefined); @@ -166,14 +165,6 @@ function AccessOrNotFoundWrapper({ setIsPolicyFeatureEnabled(isFeatureEnabled); }, [pendingField, isOffline, isFeatureEnabled]); - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (isLoadingReportData || !isPolicyNotAccessible) { - return; - } - Navigation.removeScreenFromNavigationState(SCREENS.WORKSPACE.INITIAL); - }, [isLoadingReportData, isPolicyNotAccessible]); - if (shouldShowFullScreenLoadingIndicator) { return ; } diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index d279cba2ec65..a0c4d9844eb4 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -23,12 +23,13 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {isConnectionInProgress} from '@libs/actions/connections'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName'; +import BottomTabBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; +import getTopmostRouteName from '@libs/Navigation/helpers/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar, getIcons, getPolicyExpenseChat, getReportName, getReportOfflinePendingActionAndErrors} from '@libs/ReportUtils'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; import * as App from '@userActions/App'; import * as Policy from '@userActions/Policy/Policy'; import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; @@ -70,7 +71,7 @@ type WorkspaceMenuItem = { badgeText?: string; }; -type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; +type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; type PolicyFeatureStates = Record; @@ -392,11 +393,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac return ( } > Navigation.goBack(ROUTES.HOME)} shouldShow={shouldShowNotFoundPage} subtitleKey={shouldShowPolicy ? 'workspace.common.notAuthorized' : undefined} > @@ -404,10 +406,10 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac title={policyName} onBackButtonPress={() => { if (route.params?.backTo) { - Navigation.resetToHome(); + Navigation.goBack(ROUTES.HOME); Navigation.isNavigationReady().then(() => Navigation.navigate(route.params?.backTo as Route)); } else { - Navigation.dismissModal(); + Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); } }} policyAvatar={policyAvatar} diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index df1fb63b5fd7..af306f9792e9 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -92,7 +92,8 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: if (isEmptyObject(policy)) { return; } - Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true); + // @TODO: Check if this method works the same as on the main branch + Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID)); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOnyxLoading]); diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx index cf1225d89fbf..6e8333d75451 100644 --- a/src/pages/workspace/WorkspaceJoinUserPage.tsx +++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx @@ -45,7 +45,9 @@ function WorkspaceJoinUserPage({route, policy}: WorkspaceJoinUserPageProps) { } if (!isEmptyObject(policy) && !policy?.isJoinRequestPending && !PolicyUtils.isPendingDeletePolicy(policy)) { Navigation.isNavigationReady().then(() => { - Navigation.goBack(undefined, false, true); + // @TODO: Check if this method works the same as on the main branch + // NOTE: It probably doesn't need any params. When this method is called, shouldPopAllStateOnUP is always false + Navigation.popToTop(); Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID ?? '-1')); }); return; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 231550dc0454..8d8b9c8ec918 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -35,7 +35,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -56,7 +56,7 @@ import WorkspacePageWithSections from './WorkspacePageWithSections'; type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & WithCurrentUserPersonalDetailsProps & - PlatformStackScreenProps; + PlatformStackScreenProps; /** * Inverts an object, equivalent of _.invert diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index aa540f63599e..05a5d7191e9d 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -19,7 +19,7 @@ import * as CardUtils from '@libs/CardUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getPerDiemCustomUnit, isControlPolicy} from '@libs/PolicyUtils'; import * as Category from '@userActions/Policy/Category'; import * as DistanceRate from '@userActions/Policy/DistanceRate'; @@ -40,7 +40,7 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow'; -type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; +type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; type Item = { icon: IconAsset; diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 26175c9793d9..a74c9fcef02a 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -21,6 +21,7 @@ import * as BankAccounts from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -173,8 +174,8 @@ function WorkspacePageWithSections({ shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} > Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + onLinkPress={() => Navigation.goBack(ROUTES.HOME)} shouldShow={shouldShow} subtitleKey={shouldShowPolicy ? 'workspace.common.notAuthorized' : undefined} shouldForceFullScreen diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 4a4862152d53..e3407939741a 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -12,7 +12,6 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; @@ -23,7 +22,7 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -39,14 +38,14 @@ import type {WithPolicyProps} from './withPolicy'; import withPolicy from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; -type WorkspaceProfilePageProps = WithPolicyProps & PlatformStackScreenProps; +type WorkspaceProfilePageProps = WithPolicyProps & PlatformStackScreenProps; function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: WorkspaceProfilePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const illustrations = useThemeIllustrations(); - const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); + const activeWorkspaceID = route.params.policyID; const {canUseSpotnanaTravel} = usePermissions(); const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST); @@ -136,13 +135,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac Policy.deleteWorkspace(policy?.id, policyName); setIsDeleteModalOpen(false); - - // If the workspace being deleted is the active workspace, switch to the "All Workspaces" view - if (activeWorkspaceID === policy?.id) { - setActiveWorkspaceID(undefined); - Navigation.navigateWithSwitchPolicyID({policyID: undefined}); - } - }, [policy?.id, policyName, activeWorkspaceID, setActiveWorkspaceID]); + }, [policy?.id, policyName]); return ( (); @@ -145,12 +147,6 @@ function WorkspacesListPage() { Policy.deleteWorkspace(policyIDToDelete, policyNameToDelete); setIsDeleteModalOpen(false); - - // If the workspace being deleted is the active workspace, switch to the "All Workspaces" view - if (activeWorkspaceID === policyIDToDelete) { - setActiveWorkspaceID(undefined); - Navigation.navigateWithSwitchPolicyID({policyID: undefined}); - } }; /** @@ -410,12 +406,13 @@ function WorkspacesListPage() { shouldEnableMaxHeight testID={WorkspacesListPage.displayName} shouldShowOfflineIndicatorInWideScreen + bottomContent={shouldUseNarrowLayout && } > Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.BigRocket} /> @@ -443,13 +440,14 @@ function WorkspacesListPage() { shouldEnablePickerAvoiding={false} shouldShowOfflineIndicatorInWideScreen testID={WorkspacesListPage.displayName} + bottomContent={shouldUseNarrowLayout && } > Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.BigRocket} > {!shouldUseNarrowLayout && getHeaderButton()} diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx index e094fe355218..b7ed81954af6 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx @@ -39,7 +39,7 @@ function QuickbooksExportDateSelectPage({policy}: WithPolicyConnectionsProps) { if (row.value !== exportDate) { QuickbooksOnline.updateQuickbooksOnlineExportDate(policyID, row.value, exportDate); } - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT.getRoute(policyID)); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT.getRoute(policyID)); }, [policyID, exportDate], ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 56ba8a5440b8..15476284c1a4 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -38,7 +38,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Modal from '@userActions/Modal'; @@ -56,7 +56,7 @@ type PolicyOption = ListItem & { keyForList: string; }; -type WorkspaceCategoriesPageProps = PlatformStackScreenProps; +type WorkspaceCategoriesPageProps = PlatformStackScreenProps; function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 392138a2d8d1..f370f9af5c9a 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -28,7 +28,7 @@ import WorkspaceCompanyCardsFeedPendingPage from './WorkspaceCompanyCardsFeedPen import WorkspaceCompanyCardsList from './WorkspaceCompanyCardsList'; import WorkspaceCompanyCardsListHeaderButtons from './WorkspaceCompanyCardsListHeaderButtons'; -type WorkspaceCompanyCardPageProps = PlatformStackScreenProps; +type WorkspaceCompanyCardPageProps = PlatformStackScreenProps; function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { const {translate} = useLocalize(); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 8520fd641c50..8e503cd06cf4 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -27,7 +27,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getDistanceRateCustomUnit} from '@libs/PolicyUtils'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as DistanceRate from '@userActions/Policy/DistanceRate'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; @@ -38,7 +38,7 @@ import type {Rate} from '@src/types/onyx/Policy'; type RateForList = ListItem & {value: string}; -type PolicyDistanceRatesPageProps = PlatformStackScreenProps; +type PolicyDistanceRatesPageProps = PlatformStackScreenProps; function PolicyDistanceRatesPage({ route: { diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx index a1ca4ce22e10..ea819fa96621 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx @@ -1,7 +1,7 @@ import {useRoute} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; @@ -20,7 +20,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import getClickedTargetLocation from '@libs/getClickedTargetLocation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import * as Policy from '@userActions/Policy/Policy'; import * as Report from '@userActions/Report'; @@ -40,7 +40,7 @@ type WorkspaceCardsListLabelProps = { }; function WorkspaceCardsListLabel({type, value, style}: WorkspaceCardsListLabelProps) { - const route = useRoute>(); + const route = useRoute>(); const policy = usePolicy(route.params.policyID); const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index 0e4519b578e6..7ebf7c967d72 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -19,7 +19,7 @@ import * as CardUtils from '@libs/CardUtils'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -33,7 +33,7 @@ import WorkspaceCardListRow from './WorkspaceCardListRow'; type WorkspaceExpensifyCardListPageProps = { /** Route from navigation */ - route: PlatformStackRouteProp; + route: PlatformStackRouteProp; /** List of Expensify cards */ cardsList: OnyxEntry; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx index 9ee5d8f0f9dd..d6dec6e53191 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx @@ -6,7 +6,7 @@ import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy/Policy'; @@ -16,7 +16,7 @@ import type SCREENS from '@src/SCREENS'; import WorkspaceExpensifyCardListPage from './WorkspaceExpensifyCardListPage'; import WorkspaceExpensifyCardPageEmptyState from './WorkspaceExpensifyCardPageEmptyState'; -type WorkspaceExpensifyCardPageProps = PlatformStackScreenProps; +type WorkspaceExpensifyCardPageProps = PlatformStackScreenProps; function WorkspaceExpensifyCardPage({route}: WorkspaceExpensifyCardPageProps) { const policyID = route.params.policyID ?? '-1'; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx index a7dd15d7c9a5..5d209e1467c2 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx @@ -13,7 +13,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import Navigation from '@navigation/Navigation'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -41,7 +41,7 @@ const expensifyCardFeatures: FeatureListItem[] = [ ]; type WorkspaceExpensifyCardPageEmptyStateProps = { - route: PlatformStackScreenProps['route']; + route: PlatformStackScreenProps['route']; } & WithPolicyAndFullscreenLoadingProps; function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensifyCardPageEmptyStateProps) { diff --git a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx index c65ae8957dbe..f168f0617af1 100644 --- a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx @@ -57,7 +57,7 @@ function ConfirmationStep({policyID, backTo}: ConfirmationStepProps) { if (!isSuccessful) { return; } - Navigation.navigate(backTo ?? ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID ?? '-1')); + Navigation.goBack(backTo ?? ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID ?? '-1')); Card.clearIssueNewCardFlow(); }, [backTo, policyID, isSuccessful]); diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index cc2b079fec18..d3eedabf38e1 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -5,7 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import CONST from '@src/CONST'; @@ -14,7 +14,7 @@ import WorkspaceInvoiceBalanceSection from './WorkspaceInvoiceBalanceSection'; import WorkspaceInvoiceVBASection from './WorkspaceInvoiceVBASection'; import WorkspaceInvoicingDetailsSection from './WorkspaceInvoicingDetailsSection'; -type WorkspaceInvoicesPageProps = PlatformStackScreenProps; +type WorkspaceInvoicesPageProps = PlatformStackScreenProps; function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 33ef0109a7a7..b1d41a9c9a86 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -33,7 +33,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Link from '@userActions/Link'; @@ -105,7 +105,7 @@ function generateSingleSubRateData(customUnitRates: Rate[], rateID: string, subR }; } -type WorkspacePerDiemPageProps = PlatformStackScreenProps; +type WorkspacePerDiemPageProps = PlatformStackScreenProps; function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx index f162f267a44b..cb6e0b7277c9 100644 --- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -36,7 +36,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as WorkspaceReportFieldUtils from '@libs/WorkspaceReportFieldUtils'; @@ -55,7 +55,7 @@ type ReportFieldForList = ListItem & { orderWeight?: number; }; -type WorkspaceReportFieldsPageProps = PlatformStackScreenProps; +type WorkspaceReportFieldsPageProps = PlatformStackScreenProps; function WorkspaceReportFieldsPage({ route: { diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx index 43ef6819976f..2efe8aa9760d 100644 --- a/src/pages/workspace/rules/PolicyRulesPage.tsx +++ b/src/pages/workspace/rules/PolicyRulesPage.tsx @@ -4,7 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import * as Illustrations from '@src/components/Icon/Illustrations'; @@ -13,7 +13,7 @@ import type SCREENS from '@src/SCREENS'; import ExpenseReportRulesSection from './ExpenseReportRulesSection'; import IndividualExpenseRulesSection from './IndividualExpenseRulesSection'; -type PolicyRulesPageProps = PlatformStackScreenProps; +type PolicyRulesPageProps = PlatformStackScreenProps; function PolicyRulesPage({route}: PolicyRulesPageProps) { const {translate} = useLocalize(); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index d5c72048f8a4..fce7b156824c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -37,7 +37,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Modal from '@userActions/Modal'; @@ -50,7 +50,7 @@ import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {PolicyTag, PolicyTagList, TagListItem} from './types'; -type WorkspaceTagsPageProps = PlatformStackScreenProps; +type WorkspaceTagsPageProps = PlatformStackScreenProps; function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type for the decision modal diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 9dbe739ae1db..03ca9a3ea7e5 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -32,7 +32,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -43,7 +43,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {TaxRate} from '@src/types/onyx'; -type WorkspaceTaxesPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; +type WorkspaceTaxesPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; function WorkspaceTaxesPage({ policy, diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 51ee85fd0bee..024b5d4912cc 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -3,14 +3,14 @@ import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {AuthScreensParamList, BottomTabNavigatorParamList, FullScreenNavigatorParamList, ReimbursementAccountNavigatorParamList, SettingsNavigatorParamList} from '@navigation/types'; +import type {AuthScreensParamList, ReimbursementAccountNavigatorParamList, SettingsNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@navigation/types'; import * as Policy from '@userActions/Policy/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -type NavigatorsParamList = BottomTabNavigatorParamList & AuthScreensParamList & SettingsNavigatorParamList & ReimbursementAccountNavigatorParamList & FullScreenNavigatorParamList; +type NavigatorsParamList = AuthScreensParamList & SettingsNavigatorParamList & ReimbursementAccountNavigatorParamList & WorkspaceSplitNavigatorParamList; type PolicyRouteName = | typeof SCREENS.REIMBURSEMENT_ACCOUNT_ROOT diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index c3448c2f7c95..b6821069fdf6 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -13,7 +13,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; @@ -27,7 +27,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AutoReportingFrequencyKey = Exclude, 'instant'>; type Locale = ValueOf; -type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & PlatformStackScreenProps; +type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & PlatformStackScreenProps; type WorkspaceAutoReportingFrequencyPageItem = { text: string; diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 8336700a7d79..d376f4cb7d36 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -8,7 +8,7 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; @@ -22,7 +22,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; const DAYS_OF_MONTH = 28; type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps & - PlatformStackScreenProps; + PlatformStackScreenProps; type AutoReportingOffsetKeys = ValueOf; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index f4d05a09fba6..375fc60f5f9b 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -26,7 +26,7 @@ import {getPaymentMethodDescription} from '@libs/PaymentUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {convertPolicyEmployeesToApprovalWorkflows, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils'; -import type {FullScreenNavigatorParamList} from '@navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; @@ -43,7 +43,7 @@ import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow'; import {getAutoReportingFrequencyDisplayNames} from './WorkspaceAutoReportingFrequencyPage'; import type {AutoReportingFrequencyKey} from './WorkspaceAutoReportingFrequencyPage'; -type WorkspaceWorkflowsPageProps = WithPolicyProps & PlatformStackScreenProps; +type WorkspaceWorkflowsPageProps = WithPolicyProps & PlatformStackScreenProps; function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const {translate, preferredLocale} = useLocalize(); diff --git a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx index 5241b6671e26..1ac3f6789993 100644 --- a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx +++ b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx @@ -97,13 +97,13 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic const editMembers = useCallback(() => { const backTo = approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE ? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID) : undefined; - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(policyID, backTo), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(policyID, backTo)); }, [approvalWorkflow.action, policyID]); const editApprover = useCallback( (approverIndex: number) => { const backTo = approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE ? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID) : undefined; - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverIndex, backTo), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverIndex, backTo)); }, [approvalWorkflow.action, policyID], ); @@ -114,10 +114,7 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.alias, Navigation.getActiveRoute())); return; } - Navigation.navigate( - ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverCount, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)), - CONST.NAVIGATION.ACTION_TYPE.PUSH, - ); + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverCount, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID))); }, [approverCount, policy, policyID]); return ( diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index f41a8dd0ab4d..172cee415e2a 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -1,3 +1,4 @@ +import {useNavigationState} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -19,7 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -35,7 +36,7 @@ import type {Icon} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceWorkflowsApprovalsApproverPageProps = WithPolicyAndFullscreenLoadingProps & - PlatformStackScreenProps; + PlatformStackScreenProps; type SelectionListApprover = { text: string; @@ -63,6 +64,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa const isInitialCreationFlow = approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && !route.params.backTo; const defaultApprover = policy?.approver ?? policy?.owner; const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? ''; + const rhpRoutes = useNavigationState((state) => state.routes); useEffect(() => { const currentApprover = approvalWorkflow?.approvers[approverIndex]; @@ -163,9 +165,11 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa if (approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE) { Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID)); } else { - Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); + // If in the navigation state we have a RHP page to which we can return, then we call Navigation.goBack without any parameters + const backTo = rhpRoutes.length > 1 ? undefined : ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover); + Navigation.goBack(backTo); } - }, [approvalWorkflow?.action, firstApprover, approverIndex, personalDetails, employeeList, route.params.policyID, selectedApproverEmail]); + }, [selectedApproverEmail, approvalWorkflow?.action, employeeList, personalDetails, approverIndex, route.params.policyID, rhpRoutes.length, firstApprover]); const button = useMemo(() => { let buttonText = isInitialCreationFlow ? translate('common.next') : translate('common.save'); diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx index 3f6a31e613ef..418e2ec6a222 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx @@ -11,7 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -25,7 +25,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ApprovalWorkflowEditor from './ApprovalWorkflowEditor'; type WorkspaceWorkflowsApprovalsCreatePageProps = WithPolicyAndFullscreenLoadingProps & - PlatformStackScreenProps; + PlatformStackScreenProps; function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsCreatePageProps) { const styles = useThemeStyles(); diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx index 6e512160bd7f..348fc1b3a299 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx @@ -13,7 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -28,7 +28,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ApprovalWorkflowEditor from './ApprovalWorkflowEditor'; type WorkspaceWorkflowsApprovalsEditPageProps = WithPolicyAndFullscreenLoadingProps & - PlatformStackScreenProps; + PlatformStackScreenProps; function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsEditPageProps) { const styles = useThemeStyles(); diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx index 8f7553e97fe1..0a65f7dc6b0e 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx @@ -19,7 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -48,7 +48,7 @@ type SelectionListMember = { type MembersSection = SectionListData>; type WorkspaceWorkflowsApprovalsExpensesFromPageProps = WithPolicyAndFullscreenLoadingProps & - PlatformStackScreenProps; + PlatformStackScreenProps; function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsExpensesFromPageProps) { const styles = useThemeStyles(); diff --git a/src/styles/index.ts b/src/styles/index.ts index f6b70576f48d..0c32ddbd5628 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1562,6 +1562,19 @@ const styles = (theme: ThemeColors) => ...wordBreak.breakWord, }, + searchSplitContainer: { + flex: 1, + flexDirection: 'row', + }, + + searchSidebar: { + width: variables.sideBarWidth, + height: '100%', + justifyContent: 'space-between', + borderRightWidth: 1, + borderColor: theme.border, + }, + // Sidebar Styles sidebar: { backgroundColor: theme.sidebar, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 11ff821cb240..ae40c8cc719f 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -34,7 +34,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ goBack: jest.fn(), })); -jest.mock('@src/libs/Navigation/isSearchTopmostCentralPane', () => jest.fn()); +jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); const CARLOS_EMAIL = 'cmartins@expensifail.com'; const CARLOS_ACCOUNT_ID = 1; diff --git a/tests/perf-test/SidebarLinks.perf-test.tsx b/tests/perf-test/SidebarLinks.perf-test.tsx index ad19888b47a3..3822afcfdee9 100644 --- a/tests/perf-test/SidebarLinks.perf-test.tsx +++ b/tests/perf-test/SidebarLinks.perf-test.tsx @@ -10,7 +10,7 @@ import wrapInAct from '../utils/wrapInActHelper'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; jest.mock('@libs/Permissions'); -jest.mock('@src/hooks/useActiveWorkspaceFromNavigationState'); +jest.mock('@hooks/useActiveWorkspace', () => jest.fn(() => ({activeWorkspaceID: undefined}))); jest.mock('../../src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), isActiveRoute: jest.fn(), diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index b7e14bb5bd82..07cf0090bd8f 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -20,7 +20,7 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch // Be sure to include the mocked permissions library, as some components that are rendered // during the test depend on its methods. jest.mock('@libs/Permissions'); -jest.mock('@src/hooks/useActiveWorkspaceFromNavigationState'); +jest.mock('@hooks/useActiveWorkspace', () => jest.fn(() => ({activeWorkspaceID: undefined}))); type LazyLoadLHNTestUtils = { fakePersonalDetails: PersonalDetailsList; diff --git a/tests/ui/ResizeScreenTests.tsx b/tests/ui/ResizeScreenTests.tsx index 5bd86ad152b2..15b83413bc68 100644 --- a/tests/ui/ResizeScreenTests.tsx +++ b/tests/ui/ResizeScreenTests.tsx @@ -1,26 +1,26 @@ +import type {ParamListBase} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native'; import {render, renderHook} from '@testing-library/react-native'; import React from 'react'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -import createResponsiveStackNavigator from '@libs/Navigation/AppNavigator/createResponsiveStackNavigator'; -import BottomTabNavigator from '@libs/Navigation/AppNavigator/Navigators/BottomTabNavigator'; -import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange'; +import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator'; +import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; import navigationRef from '@libs/Navigation/navigationRef'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; +import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types'; +import InitialSettingsPage from '@pages/settings/InitialSettingsPage'; import ProfilePage from '@pages/settings/Profile/ProfilePage'; -import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -const RootStack = createResponsiveStackNavigator(); +const Split = createSplitNavigator(); jest.mock('@hooks/useResponsiveLayout', () => jest.fn()); jest.mock('@libs/getIsNarrowLayout', () => jest.fn()); jest.mock('@pages/settings/InitialSettingsPage'); jest.mock('@pages/settings/Profile/ProfilePage'); -jest.mock('@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'); const DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE: ResponsiveLayoutResult = { shouldUseNarrowLayout: true, @@ -35,17 +35,16 @@ const DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE: ResponsiveLayoutResult = { }; const INITIAL_STATE = { + index: 0, routes: [ { - name: NAVIGATORS.BOTTOM_TAB_NAVIGATOR, - state: { - index: 1, - routes: [{name: SCREENS.HOME}, {name: SCREENS.SETTINGS.ROOT}], - }, + name: SCREENS.SETTINGS.ROOT, }, ], }; +const PARENT_ROUTE = {key: 'parentRouteKey', name: 'ParentNavigator'}; + const mockedGetIsNarrowLayout = getIsNarrowLayout as jest.MockedFunction; const mockedUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; @@ -55,31 +54,42 @@ describe('Resize screen', () => { mockedGetIsNarrowLayout.mockReturnValue(true); mockedUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: true}); - const {rerender} = renderHook(() => useNavigationResetRootOnLayoutChange()); - render( - - + - - - + , ); + const {rerender} = renderHook(() => + useNavigationResetOnLayoutChange({ + navigation: navigationRef.current as unknown as CustomEffectsHookProps['navigation'], + displayName: 'SplitNavigator', + descriptors: {}, + state: navigationRef.current?.getState() as CustomEffectsHookProps['state'], + }), + ); + const rootStateBeforeResize = navigationRef.current?.getRootState(); - expect(rootStateBeforeResize?.routes.at(0)?.name).toBe(NAVIGATORS.BOTTOM_TAB_NAVIGATOR); + expect(rootStateBeforeResize?.routes.at(0)?.name).toBe(SCREENS.SETTINGS.ROOT); expect(rootStateBeforeResize?.routes.at(1)).toBeUndefined(); + expect(rootStateBeforeResize?.index).toBe(0); // When resizing the screen to the wide layout mockedGetIsNarrowLayout.mockReturnValue(false); @@ -89,7 +99,8 @@ describe('Resize screen', () => { const rootStateAfterResize = navigationRef.current?.getRootState(); // Then the settings profile page should be displayed on the screen - expect(rootStateAfterResize?.routes.at(0)?.name).toBe(NAVIGATORS.BOTTOM_TAB_NAVIGATOR); + expect(rootStateAfterResize?.routes.at(0)?.name).toBe(SCREENS.SETTINGS.ROOT); expect(rootStateAfterResize?.routes.at(1)?.name).toBe(SCREENS.SETTINGS.PROFILE.ROOT); + expect(rootStateAfterResize?.index).toBe(1); }); }); diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 1de2654c09bf..ca38f9b8e64e 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -15,7 +15,6 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch // Be sure to include the mocked Permissions and Expensicons libraries or else the beta tests won't work jest.mock('@libs/Permissions'); jest.mock('@components/Icon/Expensicons'); -jest.mock('@src/hooks/useActiveWorkspaceFromNavigationState'); jest.mock('@src/hooks/useResponsiveLayout'); describe('Sidebar', () => { diff --git a/tests/unit/SidebarTest.ts b/tests/unit/SidebarTest.ts index 2c8d28c537c6..0163d175a1ce 100644 --- a/tests/unit/SidebarTest.ts +++ b/tests/unit/SidebarTest.ts @@ -13,7 +13,6 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch // Be sure to include the mocked Permissions and Expensicons libraries or else the beta tests won't work jest.mock('@src/libs/Permissions'); -jest.mock('@src/hooks/useActiveWorkspaceFromNavigationState'); jest.mock('@src/components/Icon/Expensicons'); const TEST_USER_ACCOUNT_ID = 1;