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 53197d41b85e..1e1516fd2c8b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4620,13 +4620,15 @@ 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', + OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT', }, }, 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 6274be1044b4..48a20c35c2e4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -56,7 +56,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 1896bc4f5f07..dc6b32004399 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 bb20b4abae11..3a09629ffbfd 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -15,7 +15,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'; @@ -40,6 +40,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; @@ -96,7 +99,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; @@ -135,6 +138,7 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, + bottomContent, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -312,6 +316,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 4c08c477f29d..8d78211aa747 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'; @@ -305,7 +305,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 e3f34ea3bea3..adda145fb5ff 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,18 +24,15 @@ 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 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'; @@ -60,15 +57,11 @@ 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 {workspaceSplitsWithoutEnteringAnimation} from './createResponsiveStackNavigator/GetStateForActionHandlers'; 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'; @@ -99,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 +210,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 +365,53 @@ 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 getWorkspaceSplitNavigatorOptions = ({route}: {route: RouteProp}) => { + // We don't need to do anything special for the wide screen. + if (!shouldUseNarrowLayout) { + return rootNavigatorOptions.fullScreen; + } + + // On the narrow screen, we want to animate this navigator if it is opened from the settings split. + // If it is opened from other tab, we don't want to animate it on the entry. + // There is a hook inside the workspace navigator that changes animation to SLIDE_FROM_RIGHT after entering. + // This way it can be animated properly when going back to the settings split. + const animationEnabled = !workspaceSplitsWithoutEnteringAnimation.has(route.key); + + return { + ...rootNavigatorOptions.fullScreen, + + // Allow swipe to go back from this split navigator to the settings navigator. + gestureEnabled: true, + animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE, + }; }; return ( - + + {/* This have to be the first navigator in auth screens. */} + + + - - {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 b4b71549f7ec..000000000000 --- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx +++ /dev/null @@ -1,46 +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 Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; -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 = { - animation: Animations.FADE, - 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..e718a55fb543 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx @@ -0,0 +1,72 @@ +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(); + const reportIdFromPath = currentURL && new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1); + if (reportIdFromPath) { + initialReportID = reportIdFromPath; + } else { + initialReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID)?.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..e5c2ff9a511f --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx @@ -0,0 +1,62 @@ +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 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]) => { + 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 52% rename from src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx rename to src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 86c9bce765b7..4e93f95ec837 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -1,19 +1,18 @@ -import React from 'react'; -import {View} from 'react-native'; +import React, {useEffect} from 'react'; import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; -import createCustomFullScreenNavigator from '@libs/Navigation/AppNavigator/createCustomFullScreenNavigator'; +import {workspaceSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers'; +import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator'; import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {AuthScreensParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import type NAVIGATORS from '@src/NAVIGATORS'; 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 +32,54 @@ 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 WorkspaceSplitNavigator({route, navigation}: PlatformStackScreenProps) { const rootNavigatorOptions = useRootNavigatorOptions(); + useEffect(() => { + const unsubscribe = navigation.addListener('transitionEnd', () => { + // We want to call this function only once. + unsubscribe(); + + // If we open this screen from a different tab, then it won't have animation. + if (!workspaceSplitsWithoutEnteringAnimation.has(route.key)) { + return; + } + + // We want ot set animation after mounting so it will animate on going UP to the settings split. + navigation.setOptions({animation: Animations.SLIDE_FROM_RIGHT}); + }); + + return unsubscribe; + }, [navigation, route.key]); + return ( - - - + + {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => ( + - {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => ( - - ))} - - + ))} + ); } -FullScreenNavigator.displayName = 'FullScreenNavigator'; +WorkspaceSplitNavigator.displayName = 'WorkspaceSplitNavigator'; export {CENTRAL_PANE_WORKSPACE_SCREENS}; -export default FullScreenNavigator; +export default WorkspaceSplitNavigator; 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 b478f09c2e01..f24c79c2c430 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -1,6 +1,7 @@ import React, {memo, useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithFeedback} from '@components/Pressable'; @@ -9,11 +10,14 @@ import Text from '@components/Text'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {getPreservedSplitNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState'; +import {isFullScreenName} from '@libs/Navigation/helpers'; import Navigation from '@libs/Navigation/Navigation'; -import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types'; +import type {AuthScreensParamList, RootStackParamList, State, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; @@ -23,14 +27,22 @@ import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar'; import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; 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'; +const BOTTOM_TABS = { + HOME: 'HOME', + SEARCH: 'SEARCH', + SETTINGS: 'SETTINGS', +} as const; + +type BottomTabs = ValueOf; + type BottomTabBarProps = { - selectedTab: string | undefined; + selectedTab: BottomTabs; }; /** @@ -74,6 +86,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [chatTabBrickRoad, setChatTabBrickRoad] = useState(() => getChatTabBrickRoad(activeWorkspaceID, currentReportID, reports, betas, policies, priorityMode, transactionViolations), ); @@ -85,15 +98,15 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { }, [activeWorkspaceID, transactionViolations, reports, reportActions, betas, policies, priorityMode, currentReportID]); const navigateToChats = useCallback(() => { - if (selectedTab === SCREENS.HOME) { + if (selectedTab === BOTTOM_TABS.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 === BOTTOM_TABS.SEARCH) { return; } interceptAnonymousUser(() => { @@ -120,6 +133,65 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { }); }, [activeWorkspaceID, selectedTab]); + const showSettingsPage = useCallback(() => { + const rootState = navigationRef.getRootState(); + const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name)); + + if (!topmostFullScreenRoute) { + return; + } + + const lastRouteOfTopmostFullScreenRoute = 'state' in topmostFullScreenRoute ? topmostFullScreenRoute.state?.routes.at(-1) : undefined; + + if (lastRouteOfTopmostFullScreenRoute && lastRouteOfTopmostFullScreenRoute.name === SCREENS.SETTINGS.WORKSPACES && shouldUseNarrowLayout) { + Navigation.goBack(ROUTES.SETTINGS); + return; + } + + if (topmostFullScreenRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + Navigation.goBack(ROUTES.SETTINGS); + return; + } + + interceptAnonymousUser(() => { + 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) { + // This action will put settings split under the workspace split to make sure that we can swipe back to settings split. + navigationRef.dispatch({ + type: CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT, + payload: { + policyID: 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); + }); + }, [shouldUseNarrowLayout]); + return ( <> {!!user?.isDebugModeEnabled && ( @@ -146,7 +218,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -155,7 +227,13 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { )} {translate('common.inbox')} @@ -170,7 +248,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -180,14 +258,17 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { styles.textSmall, styles.textAlignCenter, styles.mt1Half, - selectedTab === SCREENS.SEARCH.BOTTOM_TAB ? styles.textBold : styles.textSupporting, + selectedTab === BOTTOM_TABS.SEARCH ? styles.textBold : styles.textSupporting, styles.bottomTabBarLabel, ]} > {translate('common.reports')} - + @@ -199,3 +280,5 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { BottomTabBar.displayName = 'BottomTabBar'; export default memo(BottomTabBar); +export {BOTTOM_TABS}; +export type {BottomTabs}; 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 c72c4de01e4e..e0f6351e3309 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -27,6 +27,7 @@ type TopBarProps = { function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: 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 2461c542ec7d..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx +++ /dev/null @@ -1,33 +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 Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; -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: Animations.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..f656f43ac06b 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts @@ -1,108 +1,39 @@ 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, + OpenWorkspaceSplitActionType, + PushActionType, + ResponsiveStackNavigatorRouterOptions, + SwitchPolicyIdActionType, +} from './types'; + +function isOpenWorkspaceSplitAction(action: CustomRouterAction): action is OpenWorkspaceSplitActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; +} -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 +52,65 @@ 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(); 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 (isOpenWorkspaceSplitAction(action)) { + return GetStateForActionHandlers.handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter); + } + + 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..b2dea13fbd4d --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts @@ -0,0 +1,203 @@ +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 {OpenWorkspaceSplitActionType, 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, +]; + +const workspaceSplitsWithoutEnteringAnimation = new Set(); + +/** + * Handles the OPEN_WORKSPACE_SPLIT action. + * If the user is on other tab than settings and the workspace split is "remembered", this action will called after pressing the settings tab. + * It will push the settings split navigator first and then push the workspace split navigator. + * This allows the user to swipe back on the iOS to the settings split navigator underneath. + */ +function handleOpenWorkspaceSplitAction( + state: StackNavigationState, + action: OpenWorkspaceSplitActionType, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, +) { + const actionToPushSettingsSplitNavigator = StackActions.push(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, { + screen: SCREENS.SETTINGS.WORKSPACES, + }); + + const actionToPushWorkspaceSplitNavigator = StackActions.push(NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, { + screen: SCREENS.WORKSPACE.INITIAL, + params: { + policyID: action.payload.policyID, + }, + }); + + const stateWithSettingsSplitNavigator = stackRouter.getStateForAction(state, actionToPushSettingsSplitNavigator, configOptions); + + if (!stateWithSettingsSplitNavigator) { + return null; + } + + const rehydratedStateWithSettingsSplitNavigator = stackRouter.getRehydratedState(stateWithSettingsSplitNavigator, configOptions); + const stateWithWorkspaceSplitNavigator = stackRouter.getStateForAction(rehydratedStateWithSettingsSplitNavigator, actionToPushWorkspaceSplitNavigator, configOptions); + + if (!stateWithWorkspaceSplitNavigator) { + return null; + } + + const lastFullScreenRoute = stateWithWorkspaceSplitNavigator.routes.at(-1); + + if (lastFullScreenRoute?.key) { + // If the user opened the workspace split navigator from a different tab, we don't want to animate the entering transition. + // To make it feel like bottom tab navigator. + workspaceSplitsWithoutEnteringAnimation.add(lastFullScreenRoute.key); + } + + return stateWithWorkspaceSplitNavigator; +} + +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 {handleOpenWorkspaceSplitAction, handleDismissModalAction, handlePushReportAction, handlePushSearchPageAction, handleSwitchPolicyID, workspaceSplitsWithoutEnteringAnimation}; 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/TopLevelBottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/TopLevelBottomTabBar.tsx new file mode 100644 index 000000000000..af50b0c2a258 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/TopLevelBottomTabBar.tsx @@ -0,0 +1,49 @@ +import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; +import React from 'react'; +import {View} from 'react-native'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useThemeStyles from '@hooks/useThemeStyles'; +import BottomTabBar, {BOTTOM_TABS} from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; +import {isFullScreenName} from '@libs/Navigation/helpers'; +import SIDEBAR_TO_SPLIT from '@libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT'; +import type {FullScreenName} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +const FULLSCREEN_TO_TAB = { + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: BOTTOM_TABS.HOME, + [SCREENS.SEARCH.CENTRAL_PANE]: BOTTOM_TABS.SEARCH, + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: BOTTOM_TABS.SETTINGS, + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: BOTTOM_TABS.SETTINGS, +}; + +const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.CENTRAL_PANE, SCREENS.SETTINGS.WORKSPACES]; + +/** + * Currently we are using the hybrid approach for the bottom tab bar. + * On wide screen we are using per screen bottom tab bar. It gives us more flexibility. We can display the bottom tab bar on any screen without any navigation structure constraints. + * On narrow layout we display the top level bottom tab bar. It allows us to implement proper animations between screens. + */ +function TopLevelBottomTabBar() { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const topmostFullScreenRoute = useNavigationState((state) => state?.routes.findLast((route) => isFullScreenName(route.name))); + const {paddingBottom} = useStyledSafeAreaInsets(); + + // Home as fallback selected tab. + const selectedTab = FULLSCREEN_TO_TAB[(topmostFullScreenRoute?.name as FullScreenName) ?? NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]; + + // There always should be a focused screen. + const isScreenWithBottomTabFocused = useNavigationState((state) => SCREENS_WITH_BOTTOM_TAB_BAR.includes(findFocusedRoute(state)?.name ?? '')); + + const shouldDisplayTopLevelBottomTabBar = isScreenWithBottomTabFocused && shouldUseNarrowLayout; + + return ( + + + + ); +} + +export default TopLevelBottomTabBar; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx index 9ac2ffd6c8f9..ab479bf0e773 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx @@ -1,19 +1,26 @@ -import type {ParamListBase} from '@react-navigation/native'; import {createNavigatorFactory} from '@react-navigation/native'; -import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange'; +import type {ParamListBase} from '@react-navigation/native'; +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'; +import TopLevelBottomTabBar from './TopLevelBottomTabBar'; + +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, + ExtraContent: TopLevelBottomTabBar, }); 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..7c0071773495 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts @@ -0,0 +1,57 @@ +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: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; + payload: { + policyID: string; + }; + }; + +type OpenWorkspaceSplitActionType = CustomRouterActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; +}; + +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 { + OpenWorkspaceSplitActionType, + 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..cef5dd95c418 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -0,0 +1,145 @@ +import type {CommonActions, ParamListBase, PartialState, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {StackActions, StackRouter} from '@react-navigation/native'; +import isEmpty from 'lodash/isEmpty'; +import pick from 'lodash/pick'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {getParamsFromRoute} from '@libs/Navigation/helpers'; +import navigationRef from '@libs/Navigation/navigationRef'; +import type {NavigationPartialRoute} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +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 getRoutePolicyID(route: NavigationPartialRoute): string | undefined { + return (route?.params as Record | undefined)?.policyID; +} + +function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralScreen, parentRoute}}: AdaptStateIfNecessaryArgs) { + const isNarrowLayout = getIsNarrowLayout(); + + const lastRoute = state.routes.at(-1) as NavigationPartialRoute; + const routePolicyID = getRoutePolicyID(lastRoute); + + // If invalid policy page is displayed on narrow layout, sidebar screen should not be pushed to the navigation state to avoid adding reduntant not found page + if (isNarrowLayout && !!routePolicyID) { + if (PolicyUtils.shouldDisplayPolicyNotFoundPage(routePolicyID)) { + return; + } + } + + // 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)) { + const paramsFromRoute = getParamsFromRoute(sidebarScreen); + const copiedParams = pick(lastRoute?.params, paramsFromRoute); + + // We don't want to get an empty object as params because it breaks some navigation logic when comparing if routes are the same. + const params = isEmpty(copiedParams) ? undefined : copiedParams; + + // @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 eeb6db21447e..48e6094dd897 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,34 +1,38 @@ -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 {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx 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 ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; 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, + linkTo, + closeRHPFlow as originalCloseRHPFlow, + setNavigationActionToMicrotaskQueue, +} from './helpers'; import linkingConfig from './linkingConfig'; -import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState'; -import linkTo from './linkTo'; 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, State} from './types'; let allReports: OnyxCollection; Onyx.connect({ @@ -55,6 +59,9 @@ function setShouldPopAllStateOnUP(shouldPopAllStateFlag: boolean) { shouldPopAllStateOnUP = shouldPopAllStateFlag; } +/** + * Checks if the navigationRef is ready to perform a method. + */ function canNavigate(methodName: string, params: Record = {}): boolean { if (navigationRef.isReady()) { return true; @@ -63,53 +70,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 = allReports?.[`${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) { @@ -126,33 +103,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) { @@ -167,7 +119,9 @@ function getActiveRoute(): string { return ''; } - +/** + * Returns the route of a report opened in RHP. + */ function getReportRHPActiveRoute(): string { if (isReportOpenInRHP(navigationRef.getRootState())) { return getActiveRoute(); @@ -204,119 +158,188 @@ 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'); - return; + const areRouteNamesEqual = route.name === minimalAction.payload.name; + + if (!areRouteNamesEqual) { + return false; } - 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 (!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; + + /** + * Specifies whether goBack should pop to top when invoked. + * Additionaly, to execute popToTop, set the value of the global variable ShouldPopAllStateOnUP to true using the setShouldPopAllStateOnUP function. + */ + shouldPopToTop?: boolean; +}; + +const defaultGoBackOptions: Required = { + compareParams: true, + shouldPopToTop: false, +}; + +/** + * @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; } - if (shouldEnforceFallback || (isFirstRouteInNavigator && fallbackRoute)) { - navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP); + if (!navigationRef.current) { + Log.hmmm('[Navigation] Unable to go up'); return; } - const isCentralPaneFocused = isCentralPaneName(findFocusedRoute(navigationRef.current.getState())?.name); - const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? ''); + const rootState = navigationRef.current.getRootState(); + const stateFromPath = getStateFromPath(fallbackRoute); + const action = getActionFromState(stateFromPath, linkingConfig.config); - 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; - } + if (!action) { + 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 (options?.shouldPopToTop) { + if (shouldPopAllStateOnUP) { + shouldPopAllStateOnUP = false; + navigationRef.current?.dispatch(StackActions.popToTop()); + return; + } + } + + if (fallbackRoute) { + goUp(fallbackRoute, options); return; } - const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); - if (isFirstRouteInNavigator) { - goBack(route, true); + if (!navigationRef.current?.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}); } /** @@ -330,13 +353,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; @@ -350,6 +375,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 */ @@ -372,6 +398,7 @@ function setIsNavigationReady() { } /** + * @private * Checks if the navigation state contains routes that are protected (over the auth wall). * * @param state - react-navigation state object @@ -416,23 +443,80 @@ 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, ...allReports?.[`${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', + }; + + 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 getTopMostCentralPaneRouteFromRootState() { - return getTopmostCentralPaneRoute(navigationRef.getRootState() as State); +/** + * 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})); +}; + +/** + * 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()); } -function removeScreenFromNavigationState(screen: Screen) { +function removeScreenFromNavigationState(screen: string) { isNavigationReady().then(() => { - navigationRef.dispatch((state) => { + navigationRef.current?.dispatch((state) => { const routes = state.routes?.filter((item) => item.name !== screen); - return CommonActions.reset({ ...state, routes, @@ -452,7 +536,6 @@ export default { getActiveRoute, getActiveRouteWithoutParams, getReportRHPActiveRoute, - closeAndNavigate, goBack, isNavigationReady, setIsNavigationReady, @@ -461,11 +544,11 @@ export default { getTopmostReportActionId, waitForProtectedRoutes, parseHybridAppUrl, - navigateWithSwitchPolicyID, resetToHome, closeRHPFlow, setNavigationActionToMicrotaskQueue, - getTopMostCentralPaneRouteFromRootState, + navigateToReportWithPolicyCheck, + popToTop, removeScreenFromNavigationState, }; diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index b19635a77fdb..7d716630a872 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'; @@ -23,13 +23,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 */ @@ -91,7 +88,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh const currentReportIDValue = useCurrentReportID(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {setActiveWorkspaceID} = useActiveWorkspace(); const [user] = useOnyx(ONYXKEYS.USER); const isPrivateDomain = Session.isUserOnPrivateDomain(); @@ -99,6 +95,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh selector: hasCompletedGuidedSetupFlowSelector, }); + const previousAuthenticated = usePrevious(authenticated); + const initialState = useMemo(() => { if (!user || user.isFromPublicDomain) { return; @@ -107,8 +105,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh // If the user haven't completed the flow, we want to always redirect them to the onboarding flow. // We also make sure that the user is authenticated. if (!NativeModules.HybridAppModule && !isOnboardingCompleted && authenticated && !shouldShowRequire2FAModal) { - const {adaptedState} = getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config); - return adaptedState; + return getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config); } // If there is no lastVisitedPath, we can do early return. We won't modify the default behavior. @@ -126,8 +123,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh } // Otherwise we want to redirect the user to the last visited path. - const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); - return adaptedState; + return getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); // The initialState value is relevant only on the first render. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps @@ -160,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; @@ -167,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..3ab638f6808c --- /dev/null +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -0,0 +1,253 @@ +import type {NavigationState, PartialState, Route} from '@react-navigation/native'; +import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; +import pick from 'lodash/pick'; +import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +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 NAVIGATORS from '@src/NAVIGATORS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import type {Report} from '@src/types/onyx'; +import extractPolicyIDFromQuery from './extractPolicyIDFromQuery'; +import getParamsFromRoute from './getParamsFromRoute'; +import {isFullScreenName} from './isNavigatorName'; +import replacePathInNestedState from './replacePathInNestedState'; + +let allReports: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (value) => { + allReports = value; + }, +}); + +type GetAdaptedStateReturnType = 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 (!allReports?.[`${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 onboardingNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR); + const isReportSplitNavigator = fullScreenRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; + const isWorkspaceSplitNavigator = fullScreenRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR; + + // If policyID is defined, it should be passed to the reportNavigator params. + if (isReportSplitNavigator && policyID) { + const routes = []; + const reportNavigatorWithPolicyID = {...fullScreenRoute}; + reportNavigatorWithPolicyID.params = {...reportNavigatorWithPolicyID.params, policyID}; + routes.push(reportNavigatorWithPolicyID); + + return getRoutesWithIndex(routes); + } + + if (isWorkspaceSplitNavigator) { + const settingsSplitRoute = getInitialSplitNavigatorState({name: SCREENS.SETTINGS.ROOT}, {name: SCREENS.SETTINGS.WORKSPACES}); + return getRoutesWithIndex([settingsSplitRoute, ...state.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) { + const routes = [matchingRootRoute, ...state.routes]; + if (matchingRootRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + const settingsSplitRoute = getInitialSplitNavigatorState({name: SCREENS.SETTINGS.ROOT}, {name: SCREENS.SETTINGS.WORKSPACES}); + routes.unshift(settingsSplitRoute); + } + return getRoutesWithIndex(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 getRoutesWithIndex([defaultFullScreenRoute, adaptedOnboardingNavigator]); + } + + // If not, add the default full screen route. + return getRoutesWithIndex([defaultFullScreenRoute, ...state.routes]); + } + + return 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..d35480f8136a --- /dev/null +++ b/src/libs/Navigation/helpers/isNavigatorName.ts @@ -0,0 +1,48 @@ +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, + SCREENS.ONBOARDING.PRIVATE_DOMAIN, + SCREENS.ONBOARDING.WORKSPACES, +]; + +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 bd4497fbcc58..37557657b6cc 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, @@ -258,4 +259,4 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], }; -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 04ed0261a225..a91758e04711 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]: { @@ -1509,7 +1496,59 @@ 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]: { + // The path given as initialRouteName does not have route params. + // initialRouteName is not defined in this split navigator because in this case the initial route requires a policyID defined in its route params. 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 ef927e6f2cf5..000000000000 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ /dev/null @@ -1,421 +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 {OnyxCollection} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -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 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 type {Report} from '@src/types/onyx'; -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'; - -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => { - allReports = value; - }, -}); - -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 (allReports?.[`${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..483928e7011d 100644 --- a/src/libs/Navigation/linkingConfig/index.ts +++ b/src/libs/Navigation/linkingConfig/index.ts @@ -1,20 +1,13 @@ /* 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) => { - const {adaptedState} = getAdaptedStateFromPath(...args); - - // ResultState | undefined is the type this function expect. - return adaptedState; - }, - subscribe, + getStateFromPath: getAdaptedStateFromPath, 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 ecb0a2f1220b..000000000000 --- a/src/libs/Navigation/switchPolicyID.ts +++ /dev/null @@ -1,146 +0,0 @@ -import {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; - } - - 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 71f11113e84c..f5c9209982a4 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; }; @@ -1445,10 +1432,32 @@ type TravelNavigatorParamList = { }; }; -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; }; [SCREENS.WORKSPACE.PROFILE]: { policyID: string; @@ -1582,14 +1591,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; @@ -1619,52 +1622,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]: { @@ -1741,37 +1749,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, @@ -1779,8 +1789,8 @@ export type { NavigationStateRoute, NewChatNavigatorParamList, NewTaskNavigatorParamList, - OnboardingModalNavigatorParamList, OnboardingFlowName, + OnboardingModalNavigatorParamList, ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, ProfileNavigatorParamList, @@ -1790,28 +1800,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 0a352aa61b94..000000000000 --- a/src/libs/NavigationUtils.ts +++ /dev/null @@ -1,54 +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, - SCREENS.ONBOARDING.PRIVATE_DOMAIN, - SCREENS.ONBOARDING.WORKSPACES, -]); - -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 61af079f9ed1..504ac353d1c4 100644 --- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts +++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts @@ -1,22 +1,18 @@ import {NativeModules} from 'react-native'; import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; 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 {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import {updateLastVisitedPath} from '@userActions/App'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OnyxUpdatesFromServer, Report} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import PushNotification from '..'; let lastVisitedPath: string | undefined; @@ -30,15 +26,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => { - allReports = value; - }, -}); - function getLastUpdateIDAppliedToClient(): Promise { return new Promise((resolve) => { Onyx.connect({ @@ -86,9 +73,6 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati Log.info('[PushNotification] Navigating to report', false, {reportID, reportActionID}); const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; - const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); Navigation.isNavigationReady() .then(Navigation.waitForProtectedRoutes) @@ -106,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/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 4982e8660dec..ebcd61626f82 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -55,6 +55,7 @@ type ConnectionWithLastSyncData = { let allPolicies: OnyxCollection; let activePolicyId: OnyxEntry; +let isLoadingReportData = true; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -67,6 +68,12 @@ Onyx.connect({ callback: (value) => (activePolicyId = value), }); +Onyx.connect({ + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + initWithStoredValues: false, + callback: (value) => (isLoadingReportData = value ?? false), +}); + /** * Filter out the active policies, which will exclude policies with pending deletion * and policies the current user doesn't belong to. @@ -1165,6 +1172,17 @@ function areAllGroupPoliciesExpenseChatDisabled(policies = allPolicies) { return !groupPolicies.some((policy) => !!policy?.isPolicyExpenseChatEnabled); } +// eslint-disable-next-line rulesdir/no-negated-variables +function shouldDisplayPolicyNotFoundPage(policyID: string): boolean { + const policy = getPolicy(policyID); + + if (!policy) { + return false; + } + + return !isPolicyAccessible(policy) && !isLoadingReportData; +} + export { canEditTaxRate, extractPolicyIDFromPath, @@ -1288,6 +1306,7 @@ export { getUserFriendlyWorkspaceType, isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, + shouldDisplayPolicyNotFoundPage, getManagerAccountEmail, getRuleApprovers, }; 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 c20ec7386b0a..2e98c0d8d754 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -76,8 +76,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'; @@ -4256,8 +4257,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 284abf31a896..32ef887aa0ea 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'; @@ -3890,7 +3890,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); } @@ -3948,7 +3948,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); @@ -4154,10 +4154,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)); } @@ -4734,7 +4734,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); } @@ -4802,7 +4802,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); } @@ -5374,7 +5374,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); } @@ -5556,7 +5556,7 @@ function createDistanceRequest(distanceRequestInformation: 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); } @@ -7121,7 +7121,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); } @@ -7134,7 +7134,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 ab924906352e..29b30552ae0b 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,14 +76,12 @@ 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'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; 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'; @@ -1144,10 +1142,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}); } /** @@ -1489,7 +1486,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 @@ -1658,7 +1655,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)); } } @@ -2467,10 +2465,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.goBack(undefined, {shouldPopToTop: true}); + } else { + Navigation.goBack(); } - Navigation.goBack(undefined, undefined, shouldPopToTop); navigateToConciergeChat(); InteractionManager.runAfterInteractions(() => { deleteReport(reportID, shouldDeleteChildReports); @@ -2654,12 +2655,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) { @@ -2873,7 +2869,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. @@ -2907,7 +2903,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 1dbb01b008dd..0cf18ae1af93 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 9aa0f07dc59c..0da7612bd2fa 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'; @@ -39,7 +39,7 @@ Onyx.connect({ */ function startOnboardingFlow(isPrivateDomain?: boolean) { const currentRoute = navigationRef.getCurrentRoute(); - const {adaptedState} = getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config, false); + const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config, false); const focusedRoute = findFocusedRoute(adaptedState as PartialState>); if (focusedRoute?.name === currentRoute?.name) { return; 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..9b72ee30de57 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.goBack(undefined, {shouldPopToTop: true}); if (getIsSmallScreenWidth()) { Navigation.navigate(ROUTES.SETTINGS); } diff --git a/src/libs/navigateAfterJoinRequest/index.ts b/src/libs/navigateAfterJoinRequest/index.ts index 42e91d18c6ba..a3ac50cd59be 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.goBack(undefined, {shouldPopToTop: true}); 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..9b72ee30de57 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.goBack(undefined, {shouldPopToTop: true}); 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 1a082310ff53..116c360abfc4 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 2b4f1a2dc561..ef7ad93c1530 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -6,19 +6,18 @@ 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, {BOTTOM_TABS} 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'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import SearchSelectionModeHeader from './SearchSelectionModeHeader'; import SearchTypeMenu from './SearchTypeMenu'; @@ -26,11 +25,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 +73,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 +96,8 @@ function SearchPageBottomTab() { return ( } > {!selectionMode?.isEnabled ? ( <> @@ -136,15 +132,12 @@ function SearchPageBottomTab() { ) : ( )} - {shouldUseNarrowLayout && ( - - )} + ); } diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx index cb52c52cb64c..267ac7ad3fe8 100644 --- a/src/pages/WorkspaceSwitcherPage/index.tsx +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -15,8 +15,8 @@ import Navigation 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'; @@ -36,7 +36,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); @@ -85,7 +85,6 @@ function WorkspaceSwitcherPage() { } const newPolicyID = policyID === activeWorkspaceID ? undefined : policyID; - setActiveWorkspaceID(newPolicyID); Navigation.goBack(); if (newPolicyID !== activeWorkspaceID) { // On native platforms, we will see a blank screen if we navigate to a new HomeScreen route while navigating back at the same time. @@ -93,7 +92,7 @@ function WorkspaceSwitcherPage() { switchPolicyAfterInteractions(newPolicyID); } }, - [activeWorkspaceID, setActiveWorkspaceID, isFocused], + [activeWorkspaceID, isFocused], ); const usersWorkspaces = useMemo(() => { diff --git a/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx index a3df127564b1..6e389d05833f 100644 --- a/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx +++ b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx @@ -1,9 +1,10 @@ import {InteractionManager} from 'react-native'; -import Navigation from '@libs/Navigation/Navigation'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; function switchPolicyAfterInteractions(newPolicyID: string | undefined) { InteractionManager.runAfterInteractions(() => { - Navigation.navigateWithSwitchPolicyID({policyID: newPolicyID}); + navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID, payload: {policyID: newPolicyID}}); }); } diff --git a/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx index 612759a8601c..43fd1d2c7980 100644 --- a/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx +++ b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx @@ -1,7 +1,8 @@ -import Navigation from '@libs/Navigation/Navigation'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; function switchPolicyAfterInteractions(newPolicyID: string | undefined) { - Navigation.navigateWithSwitchPolicyID({policyID: newPolicyID}); + navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID, payload: {policyID: newPolicyID}}); } export default switchPolicyAfterInteractions; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e9771189bed2..3d8985451033 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; @@ -302,7 +302,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro Navigation.dismissModal(); return; } - Navigation.goBack(ROUTES.HOME, false, true); + // @TODO: Handle popToTop here + Navigation.goBack(ROUTES.HOME, {shouldPopToTop: true}); }, [isInNarrowPaneModal]); let headerView = ( @@ -607,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.goBack(undefined, {shouldPopToTop: true}); } 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 6364095d81c8..90018d5ab7e3 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,13 +19,13 @@ 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 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'; @@ -161,7 +161,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); @@ -703,7 +703,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 4b62c0e985fb..5778909f05d7 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 ?? CONST.DEFAULT_NUMBER_ID}`, { 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..095e1fdb40df 100644 --- a/src/pages/home/sidebar/BottomTabAvatar.tsx +++ b/src/pages/home/sidebar/BottomTabAvatar.tsx @@ -1,28 +1,25 @@ -import React, {useCallback} from 'react'; +import React 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 useThemeStyles from '@hooks/useThemeStyles'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import AvatarWithDelegateAvatar from './AvatarWithDelegateAvatar'; import AvatarWithOptionalStatus from './AvatarWithOptionalStatus'; import ProfileAvatarWithIndicator from './ProfileAvatarWithIndicator'; type BottomTabAvatarProps = { - /** Whether the create menu is open or not */ - isCreateMenuOpen?: boolean; - /** Whether the avatar is selected */ isSelected?: boolean; + + /** Function to call when the avatar is pressed */ + onPress: () => void; }; -function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomTabAvatarProps) { +function BottomTabAvatar({onPress, isSelected = false}: BottomTabAvatarProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); @@ -30,15 +27,6 @@ function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomT const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const emojiStatus = currentUserPersonalDetails?.status?.emojiCode ?? ''; - const showSettingsPage = useCallback(() => { - if (isCreateMenuOpen) { - // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon - return; - } - - interceptAnonymousUser(() => Navigation.navigate(ROUTES.SETTINGS)); - }, [isCreateMenuOpen]); - let children; if (delegateEmail) { @@ -68,7 +56,7 @@ function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomT return ( currentReportIDRef.current === reportID, []); - return ( { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); @@ -33,19 +40,37 @@ function BaseSidebarScreen() { return; } - Navigation.navigateWithSwitchPolicyID({policyID: undefined}); + // Otherwise, if the workspace is already loaded, we don't need to do anything + 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; + } + + 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]); + }, [activeWorkspace, activeWorkspaceID, currentRoute.key]); const shouldDisplaySearch = shouldUseNarrowLayout; return ( } > {({insets}) => ( <> diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index f5cba2cb0edd..2df1d96f6653 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'; @@ -24,9 +24,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as QuickActionNavigation from '@libs/actions/QuickActionNavigation'; 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'; @@ -44,21 +42,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 6083727cf2ad..5fa85ae4a841 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..4e5b05b5321e 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, {BOTTOM_TABS} 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'; @@ -96,7 +99,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 +348,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 +359,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 +425,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..ff15dcdeffbd 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -99,7 +99,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); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 95449e4c10ea..da652940f921 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -23,19 +23,19 @@ 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, {BOTTOM_TABS} 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'; import CONST from '@src/CONST'; 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 {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -70,7 +70,7 @@ type WorkspaceMenuItem = { badgeText?: string; }; -type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; +type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; type PolicyFeatureStates = Record; @@ -389,27 +389,23 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac }; }, [policy]); + const shouldShowBottomTab = !shouldShowNotFoundPage; + return ( : null} > Navigation.goBack(ROUTES.HOME)} shouldShow={shouldShowNotFoundPage} subtitleKey={shouldShowPolicy ? 'workspace.common.notAuthorized' : undefined} > { - if (route.params?.backTo) { - Navigation.resetToHome(); - Navigation.isNavigationReady().then(() => Navigation.navigate(route.params?.backTo as Route)); - } else { - Navigation.dismissModal(); - } - }} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} policyAvatar={policyAvatar} style={styles.headerBarDesktopHeight} /> @@ -454,7 +450,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac title={getReportName(currentUserPolicyExpenseChat)} description={translate('workspace.common.workspace')} icon={getIcons(currentUserPolicyExpenseChat, personalDetails)} - onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(currentUserPolicyExpenseChat?.reportID ?? '-1'), CONST.NAVIGATION.TYPE.UP)} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(currentUserPolicyExpenseChat?.reportID ?? '-1'), CONST.NAVIGATION.ACTION_TYPE.REPLACE)} shouldShowRightIcon wrapperStyle={[styles.br2, styles.pl2, styles.pr0, styles.pv3, styles.mt1, styles.alignItemsCenter]} shouldShowSubscriptAvatar diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 5b9d64d52cbc..9023b161e9a5 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..66caff7263ef 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.goBack(undefined, {shouldPopToTop: true}); Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID ?? '-1')); }); return; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 6b88f3b59995..791c3ce6b233 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -36,7 +36,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; 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'; @@ -57,7 +57,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 ddcb89064c7e..28d40a778167 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 845937ceaf75..026ba5ecd648 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,13 @@ 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 {canUseSpotnanaTravel, canUseWorkspaceDowngrade} = usePermissions(); const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST); @@ -166,13 +164,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 ( (); const [policyNameToDelete, setPolicyNameToDelete] = useState(); @@ -145,12 +143,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 +402,13 @@ function WorkspacesListPage() { shouldEnableMaxHeight testID={WorkspacesListPage.displayName} shouldShowOfflineIndicatorInWideScreen + bottomContent={shouldUseNarrowLayout && } > Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.BigRocket} shouldUseHeadlineHeader /> @@ -444,13 +437,14 @@ function WorkspacesListPage() { shouldEnablePickerAvoiding={false} shouldShowOfflineIndicatorInWideScreen testID={WorkspacesListPage.displayName} + bottomContent={shouldUseNarrowLayout && } > Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.BigRocket} shouldUseHeadlineHeader > 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 a8a37638f87e..b837029e933f 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -39,7 +39,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'; @@ -57,7 +57,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 19878036030b..38f6bcef5bfd 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 9def0b49e792..778519b1d35e 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 154cc53a882c..da8e84f37511 100644 --- a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx @@ -58,7 +58,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 894dc3307826..c666bfb801d0 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 ea29d41199ec..d2ef52d431ae 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 b86a35fa6fca..f8df2aacdb1c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.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'; @@ -51,7 +51,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 e588a1ecb313..6bdd8f1228f0 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -33,7 +33,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'; @@ -44,7 +44,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 99b3cf2e6baf..60f761c4fb78 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 653552452945..f6524e2827ba 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]; @@ -180,9 +182,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 2889b62d72b2..d5861895f93d 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 9468a7af4da2..c46263bf3472 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -562,6 +562,13 @@ const styles = (theme: ThemeColors) => borderRadius: variables.componentBorderRadiusLarge, }, + topLevelBottomTabBar: (shouldDisplayTopLevelBottomTabBar: boolean, bottomSafeAreaOffset: number) => ({ + position: 'absolute', + width: '100%', + paddingBottom: bottomSafeAreaOffset, + bottom: shouldDisplayTopLevelBottomTabBar ? 0 : -(bottomSafeAreaOffset + variables.bottomTabHeight), + }), + bottomTabBarContainer: { flexDirection: 'row', height: variables.bottomTabHeight, @@ -1562,6 +1569,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 05a6612e4546..8c01fcb73853 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -36,7 +36,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;