diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index cc3e256be399..dbbd7a564d7b 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -157,7 +157,7 @@ index 4286a26033..850f8944ca 100644 index ca2da6f56b..2c191598f0 100644 --- a/src/libs/Navigation/linkingConfig/prefixes.ts +++ b/src/libs/Navigation/linkingConfig/prefixes.ts - @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [ + @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [ 'https://www.expensify.cash', 'https://staging.expensify.cash', 'https://dev.new.expensify.com', 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/jest.config.js b/jest.config.js index 13645e720c8e..ea9a23f01362 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,6 +5,7 @@ module.exports = { `/tests/ui/**/*.${testFileExtension}`, `/tests/unit/**/*.${testFileExtension}`, `/tests/actions/**/*.${testFileExtension}`, + `/tests/navigation/**/*.${testFileExtension}`, `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { 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 40028a10a2da..7d9aee990627 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'; @@ -35,7 +34,6 @@ import CONFIG from './CONFIG'; import Expensify from './Expensify'; import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID'; 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'; @@ -89,8 +87,6 @@ function App({url}: AppProps) { EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, - ActiveWorkspaceContextProvider, - ReportIDsContextProvider, PlaybackContextProvider, FullScreenContextProvider, VolumeContextProvider, diff --git a/src/CONST.ts b/src/CONST.ts index 5ea39f464e66..e4f782be13d6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4,6 +4,7 @@ import {sub as dateSubtract} from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; import type {ValueOf} from 'type-fest'; +import type ResponsiveLayoutResult from './hooks/useResponsiveLayout/types'; import type {Video} from './libs/actions/Report'; import type {MileageRate} from './libs/DistanceRequestUtils'; import BankAccount from './libs/models/BankAccount'; @@ -4677,13 +4678,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: { @@ -6456,6 +6459,21 @@ const CONST = { GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip', }, SMART_BANNER_HEIGHT: 152, + + NAVIGATION_TESTS: { + DEFAULT_PARENT_ROUTE: {key: 'parentRouteKey', name: 'ParentNavigator'}, + DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE: { + shouldUseNarrowLayout: true, + isSmallScreenWidth: true, + isInNarrowPaneModal: false, + isExtraSmallScreenHeight: false, + isMediumScreenWidth: false, + isLargeScreenWidth: false, + isExtraSmallScreenWidth: false, + isSmallScreen: false, + onboardingIsMediumOrLargerScreenWidth: false, + } as ResponsiveLayoutResult, + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; 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/ROUTES.ts b/src/ROUTES.ts index 7bf1bf1c9e07..0e68d76dc621 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -143,7 +143,7 @@ const ROUTES = { SETTINGS_ADD_DELEGATE: 'settings/security/delegate', SETTINGS_DELEGATE_ROLE: { route: 'settings/security/delegate/:login/role/:role', - getRoute: (login: string, role?: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}` as const, + getRoute: (login: string, role?: string, backTo?: string) => getUrlWithBackToParam(`settings/security/delegate/${encodeURIComponent(login)}/role/${role}`, backTo), }, SETTINGS_UPDATE_DELEGATE_ROLE: { route: 'settings/security/delegate/:login/update-role/:currentRole', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2359324c9b90..6ac35016c928 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -31,7 +31,7 @@ const SCREENS = { TRIP_DETAILS: 'Travel_TripDetails', }, SEARCH: { - CENTRAL_PANE: 'Search_Central_Pane', + ROOT: 'Search_Root', REPORT_RHP: 'Search_Report_RHP', ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP', @@ -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..140c21ff8dd4 100644 --- a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx +++ b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx @@ -1,11 +1,14 @@ import {createContext} from 'react'; type ActiveWorkspaceContextType = { - activeWorkspaceID?: string; - setActiveWorkspaceID: (activeWorkspaceID?: string) => void; + activeWorkspaceID: string | undefined; + setActiveWorkspaceID: (workspaceID: string | undefined) => void; }; -const ActiveWorkspaceContext = createContext({activeWorkspaceID: undefined, setActiveWorkspaceID: () => undefined}); +const ActiveWorkspaceContext = createContext({ + activeWorkspaceID: undefined, + setActiveWorkspaceID: () => {}, +}); export default ActiveWorkspaceContext; -export {type ActiveWorkspaceContextType}; +export type {ActiveWorkspaceContextType}; diff --git a/src/components/ActiveWorkspaceProvider/index.tsx b/src/components/ActiveWorkspaceProvider/index.tsx index bc7260cdf10b..e34e8091c0f7 100644 --- a/src/components/ActiveWorkspaceProvider/index.tsx +++ b/src/components/ActiveWorkspaceProvider/index.tsx @@ -1,13 +1,24 @@ -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/getPolicyIDFromState'; +import type {RootNavigatorParamList, 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( () => ({ activeWorkspaceID, + + // We are exporting setActiveWorkspace to speedup updating this value after changing activeWorkspaceID to avoid flickering of workspace avatar. setActiveWorkspaceID, }), [activeWorkspaceID, setActiveWorkspaceID], 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..2f5bbd29e58e 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/shouldPreventDeeplinkPrompt'; 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/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 04bc4847a00f..f24be86c3b56 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -1,3 +1,4 @@ +import {useNavigationState} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useRef} from 'react'; // eslint-disable-next-line no-restricted-imports @@ -6,8 +7,6 @@ import {Platform} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; -import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; -import useIsCurrentRouteHome from '@hooks/useIsCurrentRouteHome'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +14,7 @@ import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; import {PressableWithoutFeedback} from './Pressable'; import {useProductTrainingContext} from './ProductTrainingContext'; import EducationalTooltip from './Tooltip/EducationalTooltip'; @@ -58,9 +58,12 @@ type FloatingActionButtonProps = { /* An accessibility role for the button */ role: Role; + + /* If the tooltip is allowed to be shown */ + isTooltipAllowed: boolean; }; -function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) { +function FloatingActionButton({onPress, isActive, accessibilityLabel, role, isTooltipAllowed}: FloatingActionButtonProps, ref: ForwardedRef) { const {success, buttonDefaultBG, textLight, textDark} = useTheme(); const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; @@ -68,13 +71,12 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const {shouldUseNarrowLayout} = useResponsiveLayout(); const platform = getPlatform(); const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB; - const isFocused = useBottomTabIsFocused(); const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED, {initialValue: false}); - const isActiveRouteHome = useIsCurrentRouteHome(); + const isActiveRouteHome = useNavigationState((state) => state?.routes.some((route) => route.name === SCREENS.HOME)); const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP, // On Home screen, We need to wait for the sidebar to load before showing the tooltip because there is the Concierge tooltip which is higher priority - isFocused && (!isActiveRouteHome || isSidebarLoaded), + isTooltipAllowed && (!isActiveRouteHome || isSidebarLoaded), ); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; 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 9f14c49929c3..706c46254436 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/isNavigatorName'; import CONST from '@src/CONST'; import type FocusTrapProps from './FocusTrapProps'; @@ -18,8 +18,8 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) { if (typeof focusTrapSettings?.active !== 'undefined') { 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)) { + // Focus trap can't be active on sidebar screens because it would block access to the tab bar. + 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..7941c2c575a9 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, @@ -31,7 +29,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.WORKSPACE.EXPENSIFY_CARD, SCREENS.WORKSPACE.COMPANY_CARDS, SCREENS.WORKSPACE.DISTANCE_RATES, - SCREENS.SEARCH.CENTRAL_PANE, + SCREENS.SEARCH.ROOT, SCREENS.SETTINGS.TROUBLESHOOT, SCREENS.SETTINGS.SAVE_THE_WORLD, SCREENS.WORKSPACE.RULES, 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/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 7526720bddc0..c3a274768b57 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -106,7 +106,7 @@ function KYCWall({ if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { BankAccounts.openPersonalBankAccountSetupView(); } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { - Navigation.navigate(addDebitCardRoute); + Navigation.navigate(addDebitCardRoute ?? ROUTES.HOME); } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { if (iouReport && ReportUtils.isIOUReport(iouReport)) { const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = Policy.createWorkspaceFromIOUPayment(iouReport) ?? {}; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 2dc93aa8c9c1..9a377e004802 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -16,9 +16,9 @@ import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; -import useIsCurrentRouteHome from '@hooks/useIsCurrentRouteHome'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useRootNavigationState from '@hooks/useRootNavigationState'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -43,6 +43,7 @@ import FreeTrial from '@pages/settings/Subscription/FreeTrial'; import variables from '@styles/variables'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OptionRowLHNProps} from './types'; @@ -63,16 +64,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const shouldShowWokspaceChatTooltip = isPolicyExpenseChat(report) && activePolicyID === report?.policyID && session?.accountID === report?.ownerAccountID; const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? isAdminRoom(report) : isConciergeChatReport(report); - const isActiveRouteHome = useIsCurrentRouteHome(); + + const isReportsSplitNavigatorLast = useRootNavigationState((state) => state?.routes?.at(-1)?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); const {tooltipToRender, shouldShowTooltip} = useMemo(() => { const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; const shouldShowTooltips = shouldShowWokspaceChatTooltip || shouldShowGetStartedTooltip; - const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isActiveRouteHome : isActiveRouteHome; + const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isReportsSplitNavigatorLast : isReportsSplitNavigatorLast; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return {tooltipToRender: tooltip, shouldShowTooltip: shouldShowTooltips && shouldTooltipBeVisible}; - }, [shouldShowGetStartedTooltip, shouldShowWokspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isActiveRouteHome]); + }, [shouldShowGetStartedTooltip, shouldShowWokspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isReportsSplitNavigatorLast]); const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); 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/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 01539b0d5776..88c01708df48 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -766,14 +766,14 @@ function MoneyRequestConfirmationList({ const activeRoute = Navigation.getActiveRoute(); if (option.isSelfDM) { - Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); return; } if (option.accountID) { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute), CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); } }; diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 644e00378f28..d7ecd4350aac 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -469,10 +469,7 @@ function MoneyRequestConfirmationListFooter({ return; } - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID), - CONST.NAVIGATION.ACTION_TYPE.PUSH, - ); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} diff --git a/src/components/Navigation/BottomTabBar/BOTTOM_TABS.ts b/src/components/Navigation/BottomTabBar/BOTTOM_TABS.ts new file mode 100644 index 000000000000..96eeeb5e66ea --- /dev/null +++ b/src/components/Navigation/BottomTabBar/BOTTOM_TABS.ts @@ -0,0 +1,7 @@ +const BOTTOM_TABS = { + HOME: 'HOME', + SEARCH: 'SEARCH', + SETTINGS: 'SETTINGS', +} as const; + +export default BOTTOM_TABS; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/components/Navigation/BottomTabBar/index.tsx similarity index 66% rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx rename to src/components/Navigation/BottomTabBar/index.tsx index 503b263d7cfa..8f2b0ad28390 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/components/Navigation/BottomTabBar/index.tsx @@ -1,40 +1,47 @@ 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 DebugTabView from '@components/Navigation/DebugTabView'; import {PressableWithFeedback} from '@components/Pressable'; import {useProductTrainingContext} from '@components/ProductTrainingContext'; import type {SearchQueryString} from '@components/Search/types'; import Text from '@components/Text'; import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; -import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; 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 getPlatform from '@libs/getPlatform'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import {getPreservedSplitNavigatorState} from '@navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState'; +import {isFullScreenName} from '@navigation/helpers/isNavigatorName'; +import Navigation from '@navigation/Navigation'; import navigationRef from '@navigation/navigationRef'; +import type {AuthScreensParamList, RootNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types'; 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'; +import BOTTOM_TABS from './BOTTOM_TABS'; + +type BottomTabName = ValueOf; type BottomTabBarProps = { - selectedTab: string | undefined; + selectedTab: BottomTabName; + isTooltipAllowed?: boolean; }; /** @@ -65,7 +72,7 @@ function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: stri return SearchQueryUtils.buildSearchQueryString(queryJSON); } -function BottomTabBar({selectedTab}: BottomTabBarProps) { +function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -78,15 +85,16 @@ 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), ); - const isFocused = useBottomTabIsFocused(); + const platform = getPlatform(); const isWebOrDesktop = platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP; const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.BOTTOM_NAV_INBOX_TOOLTIP, - selectedTab !== SCREENS.HOME && isFocused, + isTooltipAllowed && selectedTab !== BOTTOM_TABS.HOME, ); useEffect(() => { setChatTabBrickRoad(getChatTabBrickRoad(activeWorkspaceID, currentReportID, reports, betas, policies, priorityMode, transactionViolations)); @@ -95,24 +103,24 @@ 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; } + hideProductTrainingTooltip(); - const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/${ROUTES.HOME}` as Route) : ROUTES.HOME; - Navigation.navigate(route); - }, [activeWorkspaceID, selectedTab, hideProductTrainingTooltip]); + Navigation.navigate(ROUTES.HOME); + }, [hideProductTrainingTooltip, selectedTab]); const navigateToSearch = useCallback(() => { - if (selectedTab === SCREENS.SEARCH.BOTTOM_TAB) { + if (selectedTab === BOTTOM_TABS.SEARCH) { return; } interceptAnonymousUser(() => { - const rootState = navigationRef.getRootState() as State; - const lastSearchRoute = rootState.routes.filter((route) => route.name === SCREENS.SEARCH.CENTRAL_PANE).at(-1); + const rootState = navigationRef.getRootState() as State; + const lastSearchRoute = rootState.routes.filter((route) => route.name === SCREENS.SEARCH.ROOT).at(-1); if (lastSearchRoute) { - const {q, ...rest} = lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE]; + const {q, ...rest} = lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.ROOT]; const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID); Navigation.navigate( @@ -131,6 +139,67 @@ 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 no settings or workspace navigator route, then we should open the settings navigator. + if (!lastSettingsOrWorkspaceNavigatorRoute) { + Navigation.navigate(ROUTES.SETTINGS); + return; + } + + const state = lastSettingsOrWorkspaceNavigatorRoute.state ?? getPreservedSplitNavigatorState(lastSettingsOrWorkspaceNavigatorRoute.key); + + // 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 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 (state?.routes?.at(-1)?.name === SCREENS.SETTINGS.WORKSPACES) { + Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); + return; + } + + // Otherwise we should simply open the settings navigator. + Navigation.navigate(ROUTES.SETTINGS); + }); + }, [shouldUseNarrowLayout]); + return ( <> {!!user?.isDebugModeEnabled && ( @@ -168,7 +237,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -181,7 +250,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { styles.textSmall, styles.textAlignCenter, styles.mt1Half, - selectedTab === SCREENS.HOME ? styles.textBold : styles.textSupporting, + selectedTab === BOTTOM_TABS.HOME ? styles.textBold : styles.textSupporting, styles.bottomTabBarLabel, ]} > @@ -199,7 +268,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -209,16 +278,19 @@ 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')} - + - + @@ -228,3 +300,5 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { BottomTabBar.displayName = 'BottomTabBar'; export default memo(BottomTabBar); +export {BOTTOM_TABS}; +export type {BottomTabName}; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx similarity index 100% rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx rename to src/components/Navigation/DebugTabView.tsx diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/components/Navigation/TopBar.tsx similarity index 100% rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx rename to src/components/Navigation/TopBar.tsx diff --git a/src/components/Navigation/TopLevelBottomTabBar/index.tsx b/src/components/Navigation/TopLevelBottomTabBar/index.tsx new file mode 100644 index 000000000000..40afad43b4c3 --- /dev/null +++ b/src/components/Navigation/TopLevelBottomTabBar/index.tsx @@ -0,0 +1,78 @@ +import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; +import React, {useEffect, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; +import BottomTabBar from '@components/Navigation/BottomTabBar'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isFullScreenName, isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName'; +import {FULLSCREEN_TO_TAB, SIDEBAR_TO_SPLIT} from '@libs/Navigation/linkingConfig/RELATIONS'; +import type {FullScreenName} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import useIsBottomTabVisibleDirectly from './useIsBottomTabVisibleDirectly'; + +const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.ROOT, SCREENS.SETTINGS.WORKSPACES]; + +/** + * TopLevelBottomTabBar is displayed when the user can interact with the bottom tab bar. + * We hide it when: + * 1. The bottom tab bar is not visible. + * 2. There is transition between screens with and without the bottom tab bar. + * 3. The bottom tab bar is under the overlay. + * For cases 2 and 3, local bottom tab bar mounted on the screen will be displayed. + */ + +function TopLevelBottomTabBar() { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {paddingBottom} = useStyledSafeAreaInsets(); + const [isAfterClosingTransition, setIsAfterClosingTransition] = useState(false); + const cancelAfterInteractions = useRef | undefined>(); + + const selectedTab = useNavigationState((state) => { + const topmostFullScreenRoute = state?.routes.findLast((route) => isFullScreenName(route.name)); + return FULLSCREEN_TO_TAB[(topmostFullScreenRoute?.name as FullScreenName) ?? NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]; + }); + + // There always should be a focused screen. + const isScreenWithBottomTabFocused = useNavigationState((state) => { + const focusedRoute = findFocusedRoute(state); + + // We are checking if the focused route is a split navigator because there may be a brief moment where the navigator don't have state yet. + // That mens we don't have screen with bottom tab focused. This caused glitching. + return SCREENS_WITH_BOTTOM_TAB_BAR.includes(focusedRoute?.name ?? '') || isSplitNavigatorName(focusedRoute?.name); + }); + + const isBottomTabVisibleDirectly = useIsBottomTabVisibleDirectly(); + + const shouldDisplayTopLevelBottomTabBar = shouldUseNarrowLayout ? isScreenWithBottomTabFocused : isBottomTabVisibleDirectly; + + useEffect(() => { + cancelAfterInteractions.current?.cancel(); + + if (!shouldDisplayTopLevelBottomTabBar) { + // If the bottom tab is not visible, that means there is a screen covering it. + // In that case we need to set the flag to true because there will be a transition for which we need to wait. + setIsAfterClosingTransition(false); + } else { + // If the bottom tab should be visible, we want to wait for transition to finish. + cancelAfterInteractions.current = InteractionManager.runAfterInteractions(() => { + setIsAfterClosingTransition(true); + }); + } + }, [shouldDisplayTopLevelBottomTabBar]); + + return ( + + {/* We are not rendering BottomTabBar conditionally for two reasons + 1. It's faster to hide/show it than mount a new when needed. + 2. We need to hide tooltips as well if they were displayed. */} + + + ); +} +export default TopLevelBottomTabBar; diff --git a/src/components/Navigation/TopLevelBottomTabBar/useIsBottomTabVisibleDirectly.ts b/src/components/Navigation/TopLevelBottomTabBar/useIsBottomTabVisibleDirectly.ts new file mode 100644 index 000000000000..52b7a215d4e0 --- /dev/null +++ b/src/components/Navigation/TopLevelBottomTabBar/useIsBottomTabVisibleDirectly.ts @@ -0,0 +1,10 @@ +import {useNavigationState} from '@react-navigation/native'; +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; + +// Visible directly means not through the overlay. So the full screen (split navigator or search) has to be the last route on the root stack. +function useIsBottomTabVisibleDirectly() { + const isBottomTabVisibleDirectly = useNavigationState((state) => isFullScreenName(state?.routes.at(-1)?.name)); + return isBottomTabVisibleDirectly; +} + +export default useIsBottomTabVisibleDirectly; 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 464509b0a947..7f969e846fa5 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -14,7 +14,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, RootNavigatorParamList} from '@libs/Navigation/types'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -39,6 +39,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; @@ -95,7 +98,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; @@ -134,6 +137,7 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, + bottomContent, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -144,7 +148,7 @@ function ScreenWrapper( * so in other places where ScreenWrapper is used, we need to * fallback to useNavigation. */ - const navigationFallback = useNavigation>(); + const navigationFallback = useNavigation>(); const navigation = navigationProp ?? navigationFallback; const isFocused = useIsFocused(); const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight); @@ -316,6 +320,7 @@ function ScreenWrapper( )} + {bottomContent} diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index 78d8c5ed61fb..26e1da6c8e72 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/isNavigatorName'; 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..947dcc9c48c5 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/isSearchTopmostFullScreenRoute'; 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 03b6c820da00..00ac13749523 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/withNavigation.tsx b/src/components/withNavigation.tsx index 1d9b2a5f5cb0..32892cc51f54 100644 --- a/src/components/withNavigation.tsx +++ b/src/components/withNavigation.tsx @@ -3,10 +3,10 @@ import {useNavigation} from '@react-navigation/native'; import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React from 'react'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import type {RootStackParamList} from '@libs/Navigation/types'; +import type {RootNavigatorParamList} from '@libs/Navigation/types'; type WithNavigationProps = { - navigation: NavigationProp; + navigation: NavigationProp; }; export default function withNavigation( diff --git a/src/components/withNavigationTransitionEnd.tsx b/src/components/withNavigationTransitionEnd.tsx index 69e04ff22e35..0bb6f1ffa448 100644 --- a/src/components/withNavigationTransitionEnd.tsx +++ b/src/components/withNavigationTransitionEnd.tsx @@ -3,14 +3,14 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {useEffect, useState} from 'react'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {RootStackParamList} from '@libs/Navigation/types'; +import type {RootNavigatorParamList} from '@libs/Navigation/types'; type WithNavigationTransitionEndProps = {didScreenTransitionEnd: boolean}; export default function (WrappedComponent: ComponentType>): React.ComponentType> { function WithNavigationTransitionEnd(props: TProps, ref: ForwardedRef) { const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation = useNavigation>(); + const navigation = useNavigation>(); useEffect(() => { const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { 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..0ba5426895e1 100644 --- a/src/hooks/useActiveWorkspace.ts +++ b/src/hooks/useActiveWorkspace.ts @@ -1,6 +1,6 @@ import {useContext} from 'react'; -import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; import type {ActiveWorkspaceContextType} from '@components/ActiveWorkspace/ActiveWorkspaceContext'; +import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext'; function useActiveWorkspace(): ActiveWorkspaceContextType { 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/hooks/useBottomTabIsFocused.ts b/src/hooks/useBottomTabIsFocused.ts deleted file mode 100644 index 90b74097ecd8..000000000000 --- a/src/hooks/useBottomTabIsFocused.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native'; -import {useIsFocused, useNavigationState} from '@react-navigation/native'; -import {useEffect, useState} from 'react'; -import CENTRAL_PANE_SCREENS from '@libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS'; -import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import getTopmostFullScreenRoute from '@libs/Navigation/getTopmostFullScreenRoute'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import type {CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; -import SCREENS from '@src/SCREENS'; -import useResponsiveLayout from './useResponsiveLayout'; - -const useBottomTabIsFocused = () => { - const [isScreenFocused, setIsScreenFocused] = useState(false); - useEffect(() => { - const listener = (event: EventArg<'state', false, NavigationContainerEventMap['state']['data']>) => { - const routName = Navigation.getRouteNameFromStateEvent(event); - if (routName === SCREENS.SEARCH.CENTRAL_PANE || routName === SCREENS.SETTINGS_CENTRAL_PANE || routName === SCREENS.HOME) { - setIsScreenFocused(true); - return; - } - setIsScreenFocused(false); - }; - navigationRef.addListener('state', listener); - return () => navigationRef.removeListener('state', listener); - }, []); - - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const isFocused = useIsFocused(); - const topmostFullScreenName = useNavigationState | undefined>(getTopmostFullScreenRoute); - const topmostCentralPane = useNavigationState | undefined>(getTopmostCentralPaneRoute); - - // If there is a full screen view such as Workspace Settings or Not Found screen, the bottom tab should not be considered focused - if (topmostFullScreenName) { - return false; - } - // On the Search screen, isFocused returns false, but it is actually focused - if (shouldUseNarrowLayout) { - return isFocused || isScreenFocused; - } - // On desktop screen sizes, isFocused always returns false, so we cannot rely on it alone to determine if the bottom tab is focused - return isFocused || Object.keys(CENTRAL_PANE_SCREENS).includes(topmostCentralPane?.name ?? ''); -}; - -export default useBottomTabIsFocused; diff --git a/src/hooks/useIsCurrentRouteHome.ts b/src/hooks/useIsCurrentRouteHome.ts deleted file mode 100644 index e4950a9accc7..000000000000 --- a/src/hooks/useIsCurrentRouteHome.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {useNavigationState} from '@react-navigation/native'; -import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName'; -import SCREENS from '@src/SCREENS'; - -/** Determine if the current route is the home screen */ -function useIsCurrentRouteHome() { - const activeRoute = useNavigationState(getTopmostRouteName); - const isActiveRouteHome = activeRoute === SCREENS.HOME; - return isActiveRouteHome; -} - -export default useIsCurrentRouteHome; diff --git a/src/hooks/useRootNavigationState.ts b/src/hooks/useRootNavigationState.ts new file mode 100644 index 000000000000..289fe164be10 --- /dev/null +++ b/src/hooks/useRootNavigationState.ts @@ -0,0 +1,33 @@ +import type {NavigationState} from '@react-navigation/routers'; +import {useEffect, useRef, useState} from 'react'; +import navigationRef from '@libs/Navigation/navigationRef'; + +type Selector = (state: NavigationState) => T; + +/** + * Hook to get a value from the current root navigation state using a selector. + * + * @param selector Selector function to get a value from the state. + */ +function useRootNavigationState(selector: Selector): T { + const [result, setResult] = useState(() => selector(navigationRef.getRootState())); + + // We store the selector in a ref to avoid re-subscribing listeners every render + const selectorRef = useRef(selector); + + useEffect(() => { + selectorRef.current = selector; + }); + + useEffect(() => { + const unsubscribe = navigationRef.addListener('state', (e) => { + setResult(selectorRef.current(e.data.state as NavigationState)); + }); + + return unsubscribe; + }, []); + + return result; +} + +export default useRootNavigationState; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f0673a42c622..eb2acdbeddec 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -1,20 +1,20 @@ +import type {RouteProp} from '@react-navigation/native'; import {findFocusedRoute} from '@react-navigation/native'; -import React, {memo, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useEffect, useMemo, 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'; import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import TestToolsModal from '@components/TestToolsModal'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; 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/isNavigatorName'; 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 createRootStackNavigator from './createRootStackNavigator'; +import {workspaceSplitsWithoutEnteringAnimation} from './createRootStackNavigator/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({ @@ -201,7 +175,7 @@ function handleNetworkReconnect() { } } -const RootStack = createResponsiveStackNavigator(); +const RootStack = createRootStackNavigator(); // We want to delay the re-rendering for components(e.g. ReportActionCompose) // that depends on modal visibility until Modal is completely closed and its focused // When modal screen is focused, update modal visibility in Onyx @@ -234,8 +208,6 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const rootNavigatorOptions = useRootNavigatorOptions(); - const {canUseDefaultRooms} = usePermissions(); - const {activeWorkspaceID} = useActiveWorkspace(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {toggleSearch} = useSearchRouterContext(); @@ -247,17 +219,6 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie return NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.ONBOARDING_EMPLOYEES.route) && isOnboardingCompleted === true; }, [isOnboardingCompleted]); - 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); @@ -408,10 +369,33 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + // 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.splitNavigator; + } + + // 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.splitNavigator, + + // Allow swipe to go back from this split navigator to the settings navigator. + gestureEnabled: true, + animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE, + }; + }; + const clearStatus = () => { User.clearCustomStatus(); User.clearDraftCustomStatus(); }; + useEffect(() => { if (!currentUserPersonalDetails.status?.clearAfter) { return; @@ -434,24 +418,31 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie clearStatus(); }, [currentUserPersonalDetails.status?.clearAfter]); - const CentralPaneScreenOptions: PlatformStackNavigationOptions = { - ...hideKeyboardOnSwipe, - headerShown: false, - title: 'New Expensify', - web: { - // Prevent unnecessary scrolling - cardStyle: styles.cardStyleNavigator, - }, - }; - return ( - + + {/* This has 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..482cd7f29bb5 --- /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/isNavigatorName'; + +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..0b51e817a0ba --- /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/isNavigatorName'; + +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..c12b8b388566 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx @@ -0,0 +1,66 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useState} 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(); + + 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; + }); + + 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..f6b9d06edd78 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/createRootStackNavigator/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/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/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 deleted file mode 100644 index 3cbb5acb81e5..000000000000 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; -import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native'; -import type {ParamListBase} from '@react-navigation/routers'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -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 * as Welcome from '@userActions/Welcome'; -import CONST from '@src/CONST'; -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; -import syncBrowserHistory from './syncBrowserHistory'; - -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 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; - } - - // 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 shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) { - if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) { - return false; - } - const currentFocusedRoute = findFocusedRoute(state); - const targetFocusedRoute = findFocusedRoute(action?.payload); - - // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen - if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) { - Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton')); - return true; - } - - return false; -} - -function CustomRouter(options: PlatformStackRouterOptions) { - const stackRouter = StackRouter(options); - - 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) { - if (shouldPreventReset(state, action)) { - syncBrowserHistory(state); - return state; - } - return stackRouter.getStateForAction(state, action, configOptions); - }, - }; -} - -export default CustomRouter; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx deleted file mode 100644 index 2455587660ab..000000000000 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type {ExtraContentProps} from '@libs/Navigation/PlatformStackNavigation/types'; - -function SearchRoute({searchRoute, descriptors}: ExtraContentProps) { - const styles = useThemeStyles(); - - if (!searchRoute) { - return null; - } - - const key = searchRoute.key; - const descriptor = descriptors[key]; - - if (!descriptor) { - return null; - } - - return {descriptor.render()}; -} - -export default SearchRoute; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx deleted file mode 100644 index 9ac2ffd6c8f9..000000000000 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type {ParamListBase} from '@react-navigation/native'; -import {createNavigatorFactory} from '@react-navigation/native'; -import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange'; -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 CustomRouter from './CustomRouter'; -import RenderSearchRoute from './SearchRoute'; -import useStateWithSearch from './useStateWithSearch'; - -const ResponsiveStackNavigatorComponent = createPlatformStackNavigatorComponent('ResponsiveStackNavigator', { - createRouter: CustomRouter, - defaultScreenOptions: defaultPlatformStackScreenOptions, - useCustomState: useStateWithSearch, - useCustomEffects: useNavigationResetRootOnLayoutChange, - ExtraContent: RenderSearchRoute, -}); - -function createResponsiveStackNavigator() { - return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof ResponsiveStackNavigatorComponent>( - ResponsiveStackNavigatorComponent, - )(); -} - -export default createResponsiveStackNavigator; 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/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts new file mode 100644 index 000000000000..c57a79af8196 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -0,0 +1,221 @@ +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 {RootNavigatorParamList, 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.ROOT) { + const currentParams = lastRoute.params as RootNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; + 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.ROOT, { + ...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 RootNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; + 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); +} + +function handleNavigatingToModalFromModal( + state: StackNavigationState, + action: PushActionType, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, +) { + const modifiedState = {...state, routes: state.routes.slice(0, -1), index: state.index !== 0 ? state.index - 1 : 0}; + return stackRouter.getStateForAction(modifiedState, action, configOptions); +} + +export { + handleOpenWorkspaceSplitAction, + handleDismissModalAction, + handlePushReportAction, + handlePushSearchPageAction, + handleSwitchPolicyID, + handleNavigatingToModalFromModal, + workspaceSplitsWithoutEnteringAnimation, +}; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts new file mode 100644 index 000000000000..6443ceca025e --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts @@ -0,0 +1,107 @@ +import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {findFocusedRoute, StackRouter} from '@react-navigation/native'; +import type {ParamListBase} from '@react-navigation/routers'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import * as Localize from '@libs/Localize'; +import {isOnboardingFlowName} from '@libs/Navigation/helpers/isNavigatorName'; +import isSideModalNavigator from '@libs/Navigation/helpers/isSideModalNavigator'; +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 {DismissModalActionType, OpenWorkspaceSplitActionType, PushActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions, SwitchPolicyIdActionType} from './types'; + +function isOpenWorkspaceSplitAction(action: RootStackNavigatorAction): action is OpenWorkspaceSplitActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; +} + +function isSwitchPolicyIdAction(action: RootStackNavigatorAction): action is SwitchPolicyIdActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; +} + +function isPushAction(action: RootStackNavigatorAction): action is PushActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.PUSH; +} + +function isDismissModalAction(action: RootStackNavigatorAction): action is DismissModalActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL; +} + +function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) { + if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) { + return false; + } + const currentFocusedRoute = findFocusedRoute(state); + const targetFocusedRoute = findFocusedRoute(action?.payload); + + // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen + if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) { + Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton')); + return true; + } + + return false; +} + +function isNavigatingToModalFromModal(state: StackNavigationState, action: CommonActions.Action | StackActionType): action is PushActionType { + 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 RootStackRouter(options: RootStackNavigatorRouterOptions) { + const stackRouter = StackRouter(options); + const {setActiveWorkspaceID} = useActiveWorkspace(); + + return { + ...stackRouter, + getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, 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.ROOT) { + 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)) { + return GetStateForActionHandlers.handleNavigatingToModalFromModal(state, action, configOptions, stackRouter); + } + + return stackRouter.getStateForAction(state, action, configOptions); + }, + }; +} + +export default RootStackRouter; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx new file mode 100644 index 000000000000..cfac8c92e059 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx @@ -0,0 +1,33 @@ +import {createNavigatorFactory} from '@react-navigation/native'; +import type {ParamListBase} from '@react-navigation/native'; +import TopLevelBottomTabBar from '@components/Navigation/TopLevelBottomTabBar'; +import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; +import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; +import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; +import type {CustomStateHookProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; +import RootStackRouter from './RootStackRouter'; + +// This is an optimization to keep mounted only last few screens in the stack. +function useCustomRootStackNavigatorState({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 RootStackNavigatorComponent = createPlatformStackNavigatorComponent('ResponsiveStackNavigator', { + createRouter: RootStackRouter, + defaultScreenOptions: defaultPlatformStackScreenOptions, + useCustomEffects: useNavigationResetOnLayoutChange, + useCustomState: useCustomRootStackNavigatorState, + ExtraContent: TopLevelBottomTabBar, +}); + +function createRootStackNavigator() { + return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof RootStackNavigatorComponent>( + RootStackNavigatorComponent, + )(); +} + +export default createRootStackNavigator; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.ts similarity index 100% rename from src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.ts rename to src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.ts diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.web.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts similarity index 100% rename from src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.web.ts rename to src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts new file mode 100644 index 000000000000..933cb7d1336a --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts @@ -0,0 +1,56 @@ +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 RootStackNavigatorActionType = + | { + 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 = RootStackNavigatorActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; +}; + +type SwitchPolicyIdActionType = RootStackNavigatorActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; +}; + +type PushActionType = StackActionType & {type: typeof CONST.NAVIGATION.ACTION_TYPE.PUSH}; + +type DismissModalActionType = RootStackNavigatorActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL; +}; + +type RootStackNavigatorConfig = { + isSmallScreenWidth: boolean; +}; + +type RootStackNavigatorRouterOptions = StackRouterOptions; + +type RootStackNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & RootStackNavigatorConfig; + +type RootStackNavigatorAction = CommonActions.Action | StackActionType | RootStackNavigatorActionType; + +export type { + OpenWorkspaceSplitActionType, + SwitchPolicyIdActionType, + PushActionType, + DismissModalActionType, + RootStackNavigatorAction, + RootStackNavigatorActionType, + RootStackNavigatorRouterOptions, + RootStackNavigatorProps, + RootStackNavigatorConfig, +}; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts new file mode 100644 index 000000000000..3e0755635a14 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -0,0 +1,146 @@ +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/getParamsFromRoute'; +import navigationRef from '@libs/Navigation/navigationRef'; +import type {NavigationPartialRoute} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +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 === CONST.NAVIGATION.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..06ac86f40b2d --- /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'; +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/usePrepareSplitStackNavigatorChildren.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePrepareSplitStackNavigatorChildren.ts new file mode 100644 index 000000000000..ae55de29f13a --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePrepareSplitStackNavigatorChildren.ts @@ -0,0 +1,30 @@ +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; + } + + 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 86998ef4e308..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..e8467c2e02fe 100644 --- a/src/libs/Navigation/AppNavigator/useRootNavigatorOptions.ts +++ b/src/libs/Navigation/AppNavigator/useRootNavigatorOptions.ts @@ -14,6 +14,7 @@ type RootNavigatorOptions = { rightModalNavigator: PlatformStackNavigationOptions; basicModalNavigator: PlatformStackNavigationOptions; leftModalNavigator: PlatformStackNavigationOptions; + splitNavigator: PlatformStackNavigationOptions; homeScreen: PlatformStackNavigationOptions; fullScreen: PlatformStackNavigationOptions; centralPaneNavigator: PlatformStackNavigationOptions; @@ -106,10 +107,22 @@ const useRootNavigatorOptions = () => { }, }, + splitNavigator: { + ...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: { + ...StyleUtils.getNavigationModalCardStyle(), + }, + }, + }, + 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: { 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 095ed2684a6d..6d4bf76543bf 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,34 +1,37 @@ -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 {generateReportID} from '@libs/ReportUtils'; +import {shallowCompare} from '@libs/ObjectUtils'; +import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; +import {doesReportBelongToWorkspace, generateReportID} 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 {dismissModalWithReport as 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 originalCloseRHPFlow from './helpers/closeRHPFlow'; +import getPolicyIDFromState from './helpers/getPolicyIDFromState'; +import getStateFromPath from './helpers/getStateFromPath'; +import getTopmostReportParams from './helpers/getTopmostReportParams'; +import isReportOpenInRHP from './helpers/isReportOpenInRHP'; +import linkTo from './helpers/linkTo'; +import getMinimalAction from './helpers/linkTo/getMinimalAction'; +import type {LinkToOptions} from './helpers/linkTo/types'; +import setNavigationActionToMicrotaskQueue from './helpers/setNavigationActionToMicrotaskQueue'; 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, RootNavigatorParamList, State} from './types'; let allReports: OnyxCollection; Onyx.connect({ @@ -55,6 +58,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 +69,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 +102,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 +118,9 @@ function getActiveRoute(): string { return ''; } - +/** + * Returns the route of a report opened in RHP. + */ function getReportRHPActiveRoute(): string { if (isReportOpenInRHP(navigationRef.getRootState())) { return getActiveRoute(); @@ -194,9 +147,9 @@ function isActiveRoute(routePath: Route): boolean { /** * Main navigation method for redirecting to a route. - * @param [type] - Type of action to perform. Currently UP is supported. + * @param [options] - linkTo function options. They allow to specify if the replace action should be performed. */ -function navigate(route: Route = ROUTES.HOME, type?: string) { +function navigate(route: Route, options?: LinkToOptions) { if (!canNavigate('navigate', {route})) { // Store intended route if the navigator is not yet available, // we will try again after the NavigationContainer is ready @@ -204,119 +157,188 @@ function navigate(route: Route = ROUTES.HOME, type?: string) { pendingRoute = route; return; } - linkTo(navigationRef.current, route, type, isActiveRoute(route)); + + linkTo(navigationRef.current, route, options); } /** - * @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 (shouldEnforceFallback || (isFirstRouteInNavigator && fallbackRoute)) { - navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP); + 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; } - const isCentralPaneFocused = isCentralPaneName(findFocusedRoute(navigationRef.current.getState())?.name); - const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? ''); + if (!navigationRef.current) { + Log.hmmm('[Navigation] Unable to go up'); + return; + } - 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; - } + const rootState = navigationRef.current.getRootState(); + const stateFromPath = getStateFromPath(fallbackRoute); + const action = getActionFromState(stateFromPath, linkingConfig.config); - // 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; - } + if (!action) { + 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 {action: minimalAction, targetState} = getMinimalAction(action, rootState); - const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState as State); - const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateAfterPop); + if (minimalAction.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE || !targetState) { + return; + } - // 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; + const compareParams = options?.compareParams ?? defaultGoBackOptions.compareParams; + const indexOfFallbackRoute = targetState.routes.findLastIndex((route) => doesRouteMatchToMinimalActionPayload(route, minimalAction, compareParams)); - 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}); - } - } + const distanceToPop = targetState.routes.length - indexOfFallbackRoute - 1; + + // 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; } - navigationRef.current.goBack(); + /** + * 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.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; } - const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); - if (isFirstRouteInNavigator) { - goBack(route, true); + if (options?.shouldPopToTop) { + if (shouldPopAllStateOnUP) { + shouldPopAllStateOnUP = false; + navigationRef.current?.dispatch(StackActions.popToTop()); + return; + } + } + + if (fallbackRoute) { + goUp(fallbackRoute, options); return; } - goBack(); - navigate(route); + + if (!navigationRef.current?.canGoBack()) { + Log.hmmm('[Navigation] Unable to go back'); + return; + } + + 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: CONST.NAVIGATION.ACTION_TYPE.REPLACE, target: rootState.key}); } /** @@ -330,13 +352,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 +374,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 +397,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 +442,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 : !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: undefined, + 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 +535,6 @@ export default { getActiveRoute, getActiveRouteWithoutParams, getReportRHPActiveRoute, - closeAndNavigate, goBack, isNavigationReady, setIsNavigationReady, @@ -461,11 +543,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 df42aa04a12e..7a4950be2edb 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,12 @@ 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 from './helpers/customGetPathFromState'; +import getAdaptedStateFromPath from './helpers/getAdaptedStateFromPath'; +import setupCustomAndroidBackHandler from './helpers/setupCustomAndroidBackHandler'; 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 +90,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(); @@ -103,6 +101,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh }); const [hasNonPersonalPolicy] = useOnyx(ONYXKEYS.HAS_NON_PERSONAL_POLICY); + const previousAuthenticated = usePrevious(authenticated); + const initialState = useMemo(() => { if (!user || user.isFromPublicDomain) { return; @@ -111,8 +111,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, isn't part of a group workspace, & wasn't invited to NewDot. if (!NativeModules.HybridAppModule && !hasNonPersonalPolicy && !isOnboardingCompleted && !wasInvitedToNewDot && 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. @@ -130,8 +129,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 @@ -164,6 +162,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; @@ -171,16 +185,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 09f0070c59e4..000000000000 --- a/src/libs/Navigation/dismissModalWithReport.ts +++ /dev/null @@ -1,81 +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 && targetReport?.reportID !== getTopmostReportId(state)) { - const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID)); - 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'); - } - } -} - -// eslint-disable-next-line import/prefer-default-export -export {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 231e815a0016..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, key: topmostBottomTabRoute.key}; -} - -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 90% rename from src/libs/Navigation/closeRHPFlow.ts rename to src/libs/Navigation/helpers/closeRHPFlow.ts index 9bc40f51f472..608fd7c855ea 100644 --- a/src/libs/Navigation/closeRHPFlow.ts +++ b/src/libs/Navigation/helpers/closeRHPFlow.ts @@ -1,13 +1,13 @@ import type {NavigationContainerRef} from '@react-navigation/native'; import {StackActions} from '@react-navigation/native'; import Log from '@libs/Log'; +import type {RootNavigatorParamList} 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. */ -export default function closeRHPFlow(navigationRef: NavigationContainerRef) { +export default function closeRHPFlow(navigationRef: NavigationContainerRef) { if (!navigationRef.isReady()) { return; } 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 88% rename from src/libs/Navigation/extractPolicyIDFromQuery.ts rename to src/libs/Navigation/helpers/extractPolicyIDFromQuery.ts index b0ef3d393983..822d71216b8b 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 {buildSearchQueryJSON, getPolicyIDFromSearchQuery} 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..b6987f36ab94 --- /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 {RHP_TO_SETTINGS, RHP_TO_SIDEBAR, RHP_TO_WORKSPACE, SEARCH_TO_RHP} from '@libs/Navigation/linkingConfig/RELATIONS'; +import type {NavigationPartialRoute, RootNavigatorParamList} 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 (SEARCH_TO_RHP.includes(route.name)) { + const paramsFromRoute = getParamsFromRoute(SCREENS.SEARCH.ROOT); + + return { + name: SCREENS.SEARCH.ROOT, + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }; + } + + if (RHP_TO_SIDEBAR[route.name]) { + return getInitialSplitNavigatorState( + { + name: RHP_TO_SIDEBAR[route.name], + }, + undefined, + policyID ? {policyID} : undefined, + ); + } + + if (RHP_TO_WORKSPACE[route.name]) { + const paramsFromRoute = getParamsFromRoute(RHP_TO_WORKSPACE[route.name]); + + return getInitialSplitNavigatorState( + { + name: SCREENS.WORKSPACE.INITIAL, + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }, + { + name: RHP_TO_WORKSPACE[route.name], + params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined, + }, + ); + } + + if (RHP_TO_SETTINGS[route.name]) { + const paramsFromRoute = getParamsFromRoute(RHP_TO_SETTINGS[route.name]); + + return getInitialSplitNavigatorState( + { + name: SCREENS.SETTINGS.ROOT, + }, + { + name: 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.ROOT 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..f5cd3ffdd081 --- /dev/null +++ b/src/libs/Navigation/helpers/getPolicyIDFromState.ts @@ -0,0 +1,26 @@ +import type {NavigationPartialRoute, RootNavigatorParamList, 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.ROOT); + 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 92% rename from src/libs/Navigation/getStateFromPath.ts rename to src/libs/Navigation/helpers/getStateFromPath.ts index 58ec111575e8..b784b2322f74 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..83844847bc1f --- /dev/null +++ b/src/libs/Navigation/helpers/getTopmostReportParams.ts @@ -0,0 +1,37 @@ +import type {NavigationState, PartialState} from '@react-navigation/native'; +import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} 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/isNavigatorName.ts b/src/libs/Navigation/helpers/isNavigatorName.ts new file mode 100644 index 000000000000..ceea8e5525d4 --- /dev/null +++ b/src/libs/Navigation/helpers/isNavigatorName.ts @@ -0,0 +1,48 @@ +import {SIDEBAR_TO_SPLIT, SPLIT_TO_SIDEBAR} 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 FULL_SCREENS_SET = new Set([...Object.values(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.ROOT]); +const SIDEBARS_SET = new Set(Object.values(SPLIT_TO_SIDEBAR)); +const ONBOARDING_SCREENS_SET = new Set(ONBOARDING_SCREENS); +const SPLIT_NAVIGATORS_SET = new Set(Object.values(SIDEBAR_TO_SPLIT)); + +/** + * 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/isReportTopmostSplitNavigator.ts b/src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts new file mode 100644 index 000000000000..fbffc8bba7b0 --- /dev/null +++ b/src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts @@ -0,0 +1,16 @@ +import {navigationRef} from '@libs/Navigation/Navigation'; +import type {RootNavigatorParamList, State} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import {isFullScreenName} from './isNavigatorName'; + +const isReportTopmostSplitNavigator = (): boolean => { + const rootState = navigationRef.getRootState() as State; + + if (!rootState) { + return false; + } + + return rootState.routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; +}; + +export default isReportTopmostSplitNavigator; diff --git a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts new file mode 100644 index 000000000000..43077d44ab2a --- /dev/null +++ b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts @@ -0,0 +1,16 @@ +import {navigationRef} from '@libs/Navigation/Navigation'; +import type {RootNavigatorParamList, 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.ROOT; +}; + +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..6c9a1ebdc184 --- /dev/null +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -0,0 +1,151 @@ +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 {linkingConfig} from '@libs/Navigation/linkingConfig'; +import {shallowCompare} from '@libs/ObjectUtils'; +import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; +import type {NavigationPartialRoute, ReportsSplitNavigatorParamList, RootNavigatorParamList, 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'; +import type {LinkToOptions} from './types'; + +const defaultLinkToOptions: LinkToOptions = { + forceReplace: false, +}; + +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, options?: LinkToOptions) { + if (!navigation) { + throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); + } + + // We know that the options are always defined because we have default options. + const {forceReplace} = {...defaultLinkToOptions, ...options} as Required; + + 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 (forceReplace) { + 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/helpers/linkTo/types.ts b/src/libs/Navigation/helpers/linkTo/types.ts new file mode 100644 index 000000000000..20719c54001d --- /dev/null +++ b/src/libs/Navigation/helpers/linkTo/types.ts @@ -0,0 +1,16 @@ +type ActionPayloadParams = { + screen?: string; + params?: unknown; + path?: string; +}; + +type ActionPayload = { + params?: ActionPayloadParams; +}; + +type LinkToOptions = { + // To explicitly set the action type to replace. + forceReplace: boolean; +}; + +export type {ActionPayload, ActionPayloadParams, LinkToOptions}; 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 79% rename from src/libs/Navigation/linkingConfig/replacePathInNestedState.ts rename to src/libs/Navigation/helpers/replacePathInNestedState.ts index 6b50cd76446e..242632c83a55 100644 --- a/src/libs/Navigation/linkingConfig/replacePathInNestedState.ts +++ b/src/libs/Navigation/helpers/replacePathInNestedState.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {findFocusedRoute} from '@react-navigation/native'; import type {NavigationState, PartialState} from '@react-navigation/native'; -import type {RootStackParamList} from '@libs/Navigation/types'; +import type {RootNavigatorParamList} from '@libs/Navigation/types'; -function replacePathInNestedState(state: PartialState>, path: string) { +function replacePathInNestedState(state: PartialState>, path: string) { const found = findFocusedRoute(state); if (!found) { return; diff --git a/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts b/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts new file mode 100644 index 000000000000..8a6235835e08 --- /dev/null +++ b/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts @@ -0,0 +1,34 @@ +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; +import * as SearchQueryUtils from '@libs/SearchQueryUtils'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +/** + * Reset the policyID stored in the navigation state to undefined. + * It is necessary to reset this id after deleting the policy which is currently selected in the app. + */ +function resetPolicyIDInNavigationState() { + const rootState = navigationRef.getRootState(); + const lastPolicyRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR || route.name === SCREENS.SEARCH.ROOT); + + if (!lastPolicyRoute) { + return; + } + + if (lastPolicyRoute.params && 'policyID' in lastPolicyRoute.params) { + Navigation.setParams({policyID: undefined}, lastPolicyRoute.key); + return; + } + + const {q, ...rest} = lastPolicyRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.ROOT]; + const queryJSON = SearchQueryUtils.buildSearchQueryJSON(q); + if (!queryJSON || !queryJSON.policyID) { + return; + } + + delete queryJSON.policyID; + Navigation.setParams({q: SearchQueryUtils.buildSearchQueryString(queryJSON), ...rest}, lastPolicyRoute.key); +} + +export default resetPolicyIDInNavigationState; 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/isReportScreenTopmostCentralPane.ts b/src/libs/Navigation/isReportScreenTopmostCentralPane.ts deleted file mode 100644 index 6cfc13886a56..000000000000 --- a/src/libs/Navigation/isReportScreenTopmostCentralPane.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 isReportScreenTopmostCentralPane = (): boolean => { - const rootState = navigationRef.getRootState() as State; - - if (!rootState) { - return false; - } - - const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState); - return topmostCentralPaneRoute?.name === SCREENS.REPORT; -}; - -export default isReportScreenTopmostCentralPane; 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 771c62c4ca70..000000000000 --- a/src/libs/Navigation/linkTo/index.ts +++ /dev/null @@ -1,228 +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) || action.payload.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) - ) { - // 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) || action.payload.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) { - 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/linkTo/types.ts b/src/libs/Navigation/linkTo/types.ts deleted file mode 100644 index 254a4cdef2a5..000000000000 --- a/src/libs/Navigation/linkTo/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -type ActionPayloadParams = { - screen?: string; - params?: unknown; - path?: string; -}; - -type ActionPayload = { - params?: ActionPayloadParams; -}; - -export type {ActionPayload, ActionPayloadParams}; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/FULLSCREEN_TO_TAB.ts b/src/libs/Navigation/linkingConfig/RELATIONS/FULLSCREEN_TO_TAB.ts new file mode 100644 index 000000000000..b567a35c1ab8 --- /dev/null +++ b/src/libs/Navigation/linkingConfig/RELATIONS/FULLSCREEN_TO_TAB.ts @@ -0,0 +1,14 @@ +import type {BottomTabName} from '@components/Navigation/BottomTabBar'; +import BOTTOM_TABS from '@components/Navigation/BottomTabBar/BOTTOM_TABS'; +import type {FullScreenName} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +const FULLSCREEN_TO_TAB: Record = { + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: BOTTOM_TABS.HOME, + [SCREENS.SEARCH.ROOT]: BOTTOM_TABS.SEARCH, + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: BOTTOM_TABS.SETTINGS, + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: BOTTOM_TABS.SETTINGS, +}; + +export default FULLSCREEN_TO_TAB; 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 2c3b060e0835..18424800b394 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, @@ -255,4 +256,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..3f779ba351a1 --- /dev/null +++ b/src/libs/Navigation/linkingConfig/RELATIONS/index.ts @@ -0,0 +1,25 @@ +import FULLSCREEN_TO_TAB from './FULLSCREEN_TO_TAB'; +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; +} + +const RHP_TO_SETTINGS = createInverseRelation(SETTINGS_TO_RHP); +const RHP_TO_WORKSPACE = createInverseRelation(WORKSPACE_TO_RHP); +const RHP_TO_SIDEBAR = createInverseRelation(SIDEBAR_TO_RHP); +const SPLIT_TO_SIDEBAR = createInverseRelation(SIDEBAR_TO_SPLIT); + +export {SETTINGS_TO_RHP, RHP_TO_SETTINGS, RHP_TO_WORKSPACE, RHP_TO_SIDEBAR, SEARCH_TO_RHP, SIDEBAR_TO_RHP, WORKSPACE_TO_RHP, SIDEBAR_TO_SPLIT, SPLIT_TO_SIDEBAR, FULLSCREEN_TO_TAB}; 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 bdd6e58db21c..d15a949a129d 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 type {RootStackParamList} from '@navigation/types'; +import type {RouteConfig} from '@libs/Navigation/helpers/createNormalizedConfigs'; +import createNormalizedConfigs from '@libs/Navigation/helpers/createNormalizedConfigs'; +import type {RootNavigatorParamList} 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, +const config: LinkingOptions['config'] = { screens: { // Main Routes [SCREENS.VALIDATE_LOGIN]: ROUTES.VALIDATE_LOGIN, @@ -29,49 +28,9 @@ 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.SEARCH.CENTRAL_PANE]: { + [SCREENS.SEARCH.ROOT]: { 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.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { @@ -1516,7 +1475,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 31b749d35106..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}}); - } - 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 f733e3a32d68..fe79a8a66e61 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 type {RootStackParamList} from '@navigation/types'; +import customGetPathFromState from '@libs/Navigation/helpers/customGetPathFromState'; +import getAdaptedStateFromPath from '@libs/Navigation/helpers/getAdaptedStateFromPath'; +import type {RootNavigatorParamList} from '@libs/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, +const linkingConfig: LinkingOptions = { + getStateFromPath: getAdaptedStateFromPath, getPathFromState: customGetPathFromState, prefixes, config, diff --git a/src/libs/Navigation/linkingConfig/prefixes.ts b/src/libs/Navigation/linkingConfig/prefixes.ts index ca2da6f56b39..c3b52cef9852 100644 --- a/src/libs/Navigation/linkingConfig/prefixes.ts +++ b/src/libs/Navigation/linkingConfig/prefixes.ts @@ -1,8 +1,8 @@ import type {LinkingOptions} from '@react-navigation/native'; -import type {RootStackParamList} from '@libs/Navigation/types'; +import type {RootNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; -const prefixes: LinkingOptions['prefixes'] = [ +const prefixes: LinkingOptions['prefixes'] = [ 'app://-/', 'new-expensify://', 'https://www.expensify.cash', 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 8f14032e7e33..000000000000 --- a/src/libs/Navigation/linkingConfig/subscribe/index.native.ts +++ /dev/null @@ -1,41 +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(); - }; -}; - -// eslint-disable-next-line import/prefer-default-export -export {subscribe}; diff --git a/src/libs/Navigation/linkingConfig/subscribe/index.ts b/src/libs/Navigation/linkingConfig/subscribe/index.ts deleted file mode 100644 index 7ccbb5252a2c..000000000000 --- a/src/libs/Navigation/linkingConfig/subscribe/index.ts +++ /dev/null @@ -1,8 +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; - -// eslint-disable-next-line import/prefer-default-export -export {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 d3a51143eab7..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 40290552b048..4966755f77ae 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -22,10 +22,11 @@ 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'; -type NavigationRef = NavigationContainerRefWithCurrent; +type NavigationRef = NavigationContainerRefWithCurrent; -type NavigationRoot = NavigationHelpers; +type NavigationRoot = NavigationHelpers; type GoBackAction = Extract; type ResetAction = Extract; @@ -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; }; @@ -86,7 +74,6 @@ type BackToAndForwardToParms = { type SettingsNavigatorParamList = { [SCREENS.SETTINGS.SHARE_CODE]: undefined; - [SCREENS.SETTINGS.PROFILE.ROOT]: undefined; [SCREENS.SETTINGS.PROFILE.PRONOUNS]: undefined; [SCREENS.SETTINGS.PROFILE.DISPLAY_NAME]: undefined; [SCREENS.SETTINGS.PROFILE.TIMEZONE]: undefined; @@ -111,17 +98,11 @@ type SettingsNavigatorParamList = { backTo?: Routes; forwardTo?: Routes; }; - [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.APP_DOWNLOAD_LINKS]: undefined; - [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; [SCREENS.SETTINGS.CONSOLE]: { backTo: Routes; }; @@ -130,7 +111,6 @@ type SettingsNavigatorParamList = { source: string; backTo: Routes; }; - [SCREENS.SETTINGS.WALLET.ROOT]: undefined; [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined; [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: { /** cardID of selected card */ @@ -765,6 +745,7 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE]: { login: string; role?: string; + backTo?: Routes; }; [SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE]: { login: string; @@ -1492,10 +1473,32 @@ type TravelNavigatorParamList = { }; }; -type FullScreenNavigatorParamList = { +type ReportsSplitNavigatorParamList = { + [SCREENS.HOME]: undefined; + [SCREENS.REPORT]: { + reportActionID: string; + reportID: string; + openOnAdminRoom?: boolean; + referrer?: string; + }; +}; + +type SettingsSplitNavigatorParamList = { + [SCREENS.SETTINGS.ROOT]: undefined; + [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; @@ -1629,14 +1632,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; @@ -1666,52 +1663,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.ROOT]: { + q: SearchQueryString; + name?: string; }; +}; type SearchReportParamList = { [SCREENS.SEARCH.REPORT_RHP]: { @@ -1786,39 +1788,41 @@ type DebugParamList = { }; }; -type RootStackParamList = PublicScreensParamList & AuthScreensParamList & LeftModalNavigatorParamList; +type RootNavigatorParamList = 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.ROOT; + +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 RootNavigatorParamList {} + } +} 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, @@ -1826,8 +1830,8 @@ export type { NavigationStateRoute, NewChatNavigatorParamList, NewTaskNavigatorParamList, - OnboardingModalNavigatorParamList, OnboardingFlowName, + OnboardingModalNavigatorParamList, ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, ProfileNavigatorParamList, @@ -1837,28 +1841,33 @@ export type { ReportDescriptionNavigatorParamList, ReportDetailsNavigatorParamList, ReportSettingsNavigatorParamList, + ReportsSplitNavigatorParamList, + RestrictedActionParamList, RightModalNavigatorParamList, RoomMembersNavigatorParamList, - RootStackParamList, + RootNavigatorParamList, + 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 237a615b570a..4a502d78b8f7 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({ @@ -112,9 +99,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) @@ -132,10 +116,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 4b35152c7677..2c1c96829f84 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -56,6 +56,7 @@ type ConnectionWithLastSyncData = { let allPolicies: OnyxCollection; let activePolicyId: OnyxEntry; +let isLoadingReportData = true; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -68,6 +69,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. @@ -1181,6 +1188,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; +} + function hasOtherControlWorkspaces(currentPolicyID: string) { const otherControlWorkspaces = Object.values(allPolicies ?? {}).filter((policy) => policy?.id !== currentPolicyID && isPolicyAdmin(policy) && isControlPolicy(policy)); return otherControlWorkspaces.length > 0; @@ -1329,6 +1347,7 @@ export { getUserFriendlyWorkspaceType, isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, + shouldDisplayPolicyNotFoundPage, hasOtherControlWorkspaces, 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 3faa7148ed4e..2bbb38b23990 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -76,8 +76,9 @@ import {translateLocal} 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 {rand64} from './NumberUtils'; import Parser from './Parser'; import Permissions from './Permissions'; @@ -4427,8 +4428,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.ROOT) { Navigation.dismissModal(); return; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index dcb9d1a6be40..6d0e8e84c9a8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -46,7 +46,7 @@ import { import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; -import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import {buildNextStep} from '@libs/NextStepUtils'; import {rand64} from '@libs/NumberUtils'; @@ -4086,7 +4086,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { } InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : activeReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : activeReportID); if (activeReportID) { notifyNewAction(activeReportID, payeeAccountID); } @@ -4144,7 +4144,7 @@ function sendInvoice( API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - if (isSearchTopmostCentralPane()) { + if (isSearchTopmostFullScreenRoute()) { Navigation.dismissModal(); } else { Navigation.dismissModalWithReport(invoiceRoom); @@ -4350,10 +4350,10 @@ function trackExpense( } } InteractionManager.runAfterInteractions(() => 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)); } @@ -4924,7 +4924,7 @@ function splitBill({ API.write(WRITE_COMMANDS.SPLIT_BILL, parameters, onyxData); InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : existingSplitChatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : existingSplitChatReportID); notifyNewAction(splitData.chatReportID, currentUserAccountID); } @@ -4992,7 +4992,7 @@ function splitBillAndOpenReport({ API.write(WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, parameters, onyxData); InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : splitData.chatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : splitData.chatReportID); notifyNewAction(splitData.chatReportID, currentUserAccountID); } @@ -5560,7 +5560,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA API.write(WRITE_COMMANDS.COMPLETE_SPLIT_BILL, parameters, {optimisticData, successData, failureData}); InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : chatReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : chatReportID); notifyNewAction(chatReportID, sessionAccountID); } @@ -5734,7 +5734,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); const activeReportID = isMoneyRequestReport ? report?.reportID ?? '-1' : parameters.chatReportID; - Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : activeReportID); + Navigation.dismissModal(isSearchTopmostFullScreenRoute() ? undefined : activeReportID); notifyNewAction(activeReportID, userAccountID); } @@ -7320,7 +7320,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); notifyNewAction(params.chatReportID, managerID); } @@ -7333,7 +7333,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); notifyNewAction(params.chatReportID, managerID); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a469af82dd1c..a600373c83a8 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -67,15 +67,15 @@ 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/isNavigatorName'; +import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types'; 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'; import Parser from '@libs/Parser'; 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'; @@ -96,7 +96,6 @@ import { buildOptimisticTaskReportAction, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, completeShortMention, - doesReportBelongToWorkspace, findLastAccessedReport, findSelfDMReportID, formatReportLastMessageText, @@ -1139,7 +1138,6 @@ function openReport( function navigateToAndOpenReport( userLogins: string[], shouldDismissModal = true, - actionType?: string, reportName?: string, avatarUri?: string, avatarFile?: File | CustomRNImageManipulatorResult | undefined, @@ -1180,10 +1178,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}); } /** @@ -1529,7 +1526,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'), {forceReplace: true}); }; // The report screen will listen to this event and transfer the draft comment to the existing report @@ -1696,7 +1693,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)); } } @@ -2349,7 +2347,7 @@ function updateWriteCapability(report: Report, newValue: WriteCapability) { /** * Navigates to the 1:1 report with Concierge */ -function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageActive = () => true, actionType?: string) { +function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageActive = () => true, linkToOptions?: LinkToOptions) { // If conciergeChatReportID contains a concierge report ID, we navigate to the concierge chat using the stored report ID. // Otherwise, we would find the concierge chat and navigate to it. if (!conciergeChatReportID) { @@ -2360,12 +2358,12 @@ function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageA if (!checkIfCurrentPageActive()) { return; } - navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal, actionType); + navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal); }); } else if (shouldDismissModal) { Navigation.dismissModal(conciergeChatReportID); } else { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID), actionType); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID), linkToOptions); } } @@ -2516,10 +2514,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); @@ -2703,12 +2704,7 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi const onClick = () => 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) { @@ -2922,7 +2918,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. @@ -2956,7 +2952,7 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { Navigation.goBack(); } - navigateToConciergeChat(false, () => true, CONST.NAVIGATION.TYPE.UP); + navigateToConciergeChat(false, () => true, {forceReplace: true}); } } @@ -4633,6 +4629,7 @@ export { broadcastUserIsTyping, clearAddRoomMemberError, clearAvatarErrors, + clearDeleteTransactionNavigateBackUrl, clearGroupChat, clearIOUError, clearNewRoomFormError, @@ -4650,6 +4647,7 @@ export { exportReportToCSV, exportToIntegration, flagComment, + getConciergeReportID, getCurrentUserAccountID, getDraftPrivateNote, getMostRecentReportID, @@ -4685,6 +4683,7 @@ export { saveReportActionDraft, saveReportDraftComment, searchInServer, + setDeleteTransactionNavigateBackUrl, setGroupDraft, setIsComposerFullSize, setLastOpenedPublicRoom, @@ -4712,7 +4711,4 @@ export { updateReportName, updateRoomVisibility, updateWriteCapability, - getConciergeReportID, - setDeleteTransactionNavigateBackUrl, - clearDeleteTransactionNavigateBackUrl, }; diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts index add7bf271795..096ae001cdef 100644 --- a/src/libs/actions/Welcome/OnboardingFlow.ts +++ b/src/libs/actions/Welcome/OnboardingFlow.ts @@ -1,10 +1,10 @@ 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/getAdaptedStateFromPath'; 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 type {RootNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -39,8 +39,8 @@ Onyx.connect({ */ function startOnboardingFlow(isPrivateDomain?: boolean) { const currentRoute = navigationRef.getCurrentRoute(); - const {adaptedState} = getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config, false); - const focusedRoute = findFocusedRoute(adaptedState as PartialState>); + 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..d63fcb25aaf7 100644 --- a/src/libs/navigateAfterJoinRequest/index.desktop.ts +++ b/src/libs/navigateAfterJoinRequest/index.desktop.ts @@ -3,7 +3,7 @@ import Navigation from '@navigation/Navigation'; import ROUTES from '@src/ROUTES'; const navigateAfterJoinRequest = () => { - Navigation.goBack(undefined, false, true); + 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..60cbf64cda90 100644 --- a/src/libs/navigateAfterJoinRequest/index.ts +++ b/src/libs/navigateAfterJoinRequest/index.ts @@ -2,7 +2,7 @@ import Navigation from '@navigation/Navigation'; import ROUTES from '@src/ROUTES'; const navigateAfterJoinRequest = () => { - Navigation.goBack(undefined, false, true); + 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..d63fcb25aaf7 100644 --- a/src/libs/navigateAfterJoinRequest/index.web.ts +++ b/src/libs/navigateAfterJoinRequest/index.web.ts @@ -3,7 +3,7 @@ import Navigation from '@navigation/Navigation'; import ROUTES from '@src/ROUTES'; const navigateAfterJoinRequest = () => { - Navigation.goBack(undefined, false, true); + Navigation.goBack(undefined, {shouldPopToTop: true}); if (getIsSmallScreenWidth()) { Navigation.navigate(ROUTES.SETTINGS); } diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index a6509fcaba37..b7adc48b39d6 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -1,6 +1,6 @@ import ROUTES from '@src/ROUTES'; +import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom'; import Navigation from './Navigation/Navigation'; -import shouldOpenOnAdminRoom from './Navigation/shouldOpenOnAdminRoom'; import * as ReportUtils from './ReportUtils'; const navigateAfterOnboarding = (isSmallScreenWidth: boolean, canUseDefaultRooms: boolean | undefined, onboardingPolicyID?: string, activeWorkspaceID?: string) => { diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index b4660f21a3c9..02095f1a3afc 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/isNavigatorName'; +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/ConciergePage.tsx b/src/pages/ConciergePage.tsx index c4de1e3b4062..191930ff749c 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -13,6 +13,7 @@ import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; /* * This is a "utility page", that does this: @@ -52,7 +53,7 @@ function ConciergePage() { Report.navigateToConciergeChat(true, () => !isUnmounted.current); }); } else { - Navigation.navigate(); + Navigation.navigate(ROUTES.HOME); } }, [session, isLoadingReportData, route.params, viewTourTaskReport]), ); diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index e429cc8681be..fd617998da98 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'; @@ -71,14 +71,14 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { if (value !== '') { ReportActions.updateReportField(report.reportID, {...reportField, 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..446976f41718 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, {forceReplace: true}); return; } diff --git a/src/pages/GroupChatNameEditPage.tsx b/src/pages/GroupChatNameEditPage.tsx index 66cc4b0a2329..5bea9f1b9604 100644 --- a/src/pages/GroupChatNameEditPage.tsx +++ b/src/pages/GroupChatNameEditPage.tsx @@ -63,9 +63,7 @@ function GroupChatNameEditPage({report}: GroupChatNameEditPageProps) { if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { Report.updateGroupChatName(reportID, values[INPUT_IDS.NEW_CHAT_NAME] ?? ''); } - - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID))); - + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID))); return; } if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx index 360e76738381..ad0682a3d555 100644 --- a/src/pages/NewChatConfirmPage.tsx +++ b/src/pages/NewChatConfirmPage.tsx @@ -104,7 +104,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP } const logins: string[] = (newGroupDraft.participants ?? []).map((participant) => participant.login).filter((login): login is string => !!login); - Report.navigateToAndOpenReport(logins, true, undefined, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', avatarFile, optimisticReportID.current, true); + Report.navigateToAndOpenReport(logins, true, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', avatarFile, optimisticReportID.current, true); }, [newGroupDraft, avatarFile]); const stashedLocalAvatarImage = newGroupDraft?.avatarUri; diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index d2cdda140bb8..b34c35973452 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'; @@ -94,7 +95,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()} /> ; +type SearchPageProps = PlatformStackScreenProps; 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 && ( - <> - - - - + + + {queryJSON ? ( + + + + + ) : ( + { + clearSelectedTransactions(); + turnOffMobileSelectionMode(); + }} + /> + )} + + + + + + + + )} - + ); } SearchPage.displayName = 'SearchPage'; +SearchPage.whyDidYouRender = true; export default SearchPage; diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageNarrow.tsx similarity index 76% rename from src/pages/Search/SearchPageBottomTab.tsx rename to src/pages/Search/SearchPageNarrow.tsx index fc4a7267253a..e28b4fcf06aa 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -3,23 +3,23 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Animated, {clamp, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import BottomTabBar from '@components/Navigation/BottomTabBar'; +import BOTTOM_TABS from '@components/Navigation/BottomTabBar/BOTTOM_TABS'; +import TopBar from '@components/Navigation/TopBar'; 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 useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; 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'; @@ -27,11 +27,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 SearchPageNarrow({queryJSON, policyID, searchName}: SearchPageBottomTabProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); - const activeCentralPaneRoute = useActiveCentralPaneRoute(); + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); @@ -70,21 +76,12 @@ function SearchPageBottomTab() { [windowHeight, topBarOffset, StyleUtils.searchHeaderHeight], ); - 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) { return ( @@ -101,9 +98,9 @@ function SearchPageBottomTab() { return ( } headerGapStyles={styles.searchHeaderGap} > {!selectionMode?.isEnabled ? ( @@ -139,19 +136,16 @@ function SearchPageBottomTab() { ) : ( )} - {shouldUseNarrowLayout && ( - - )} + ); } -SearchPageBottomTab.displayName = 'SearchPageBottomTab'; +SearchPageNarrow.displayName = 'SearchPageNarrow'; -export default SearchPageBottomTab; +export default SearchPageNarrow; diff --git a/src/pages/Search/SearchTypeMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx index 5a5a79c3cbfc..ee6423b7fd90 100644 --- a/src/pages/Search/SearchTypeMenuNarrow.tsx +++ b/src/pages/Search/SearchTypeMenuNarrow.tsx @@ -86,7 +86,7 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title, useEffect(() => { const listener = (event: EventArg<'state', false, NavigationContainerEventMap['state']['data']>) => { - if (Navigation.getRouteNameFromStateEvent(event) === SCREENS.SEARCH.CENTRAL_PANE) { + if (Navigation.getRouteNameFromStateEvent(event) === SCREENS.SEARCH.ROOT) { setIsScreenFocused(true); return; } diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx index 497adef4ec1e..6ac0e87094c4 100644 --- a/src/pages/TransactionReceiptPage.tsx +++ b/src/pages/TransactionReceiptPage.tsx @@ -3,7 +3,7 @@ import {useOnyx} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types'; +import type {AuthScreensParamList, RootNavigatorParamList, State} from '@libs/Navigation/types'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -47,7 +47,7 @@ function TransactionReceipt({route}: TransactionReceiptProps) { const onModalClose = () => { // Receipt Page can be opened either from Reports or from Search RHP view // We have to handle going back to correct screens, if it was opened from RHP just close the modal, otherwise go to Report Page - const rootState = navigationRef.getRootState() as State; + const rootState = navigationRef.getRootState() as State; const secondToLastRoute = rootState.routes.at(-2); if (secondToLastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { Navigation.dismissModal(); diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx index 4f2d07689766..eec02a1c0ea8 100644 --- a/src/pages/WorkspaceSwitcherPage/index.tsx +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -39,7 +39,7 @@ function WorkspaceSwitcherPage() { const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {translate} = useLocalize(); - const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); + const {activeWorkspaceID} = useActiveWorkspace(); const isFocused = useIsFocused(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); @@ -89,7 +89,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. @@ -97,7 +96,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 0afc4b3fc122..e6ae9bcf89d0 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; @@ -285,7 +285,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { Navigation.dismissModal(); return; } - Navigation.goBack(undefined, false, true); + Navigation.goBack(undefined, {shouldPopToTop: true}); }, [isInNarrowPaneModal]); let headerView = ( @@ -589,7 +589,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { Navigation.setShouldPopAllStateOnUP(true); - Navigation.goBack(undefined, false, true); + 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 e64352720ea9..2ffd3c06c77a 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -5,8 +5,8 @@ import type {DebouncedFunc} from 'lodash'; import React, {memo, useCallback, useContext, useEffect, useLayoutEffect, 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 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'; @@ -20,14 +20,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; import {getChatFSAttributes} from '@libs/Fullstory'; -import isReportScreenTopmostCentralPane from '@libs/Navigation/isReportScreenTopmostCentralPane'; -import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; +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 {PersonalDetailsContext} from '@src/components/OnyxProvider'; @@ -164,7 +164,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); @@ -430,7 +430,7 @@ function ReportActionsList({ InteractionManager.runAfterInteractions(() => { // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where // they are now in the list. - if (!isFromCurrentUser || !isReportScreenTopmostCentralPane()) { + if (!isFromCurrentUser || !isReportTopmostSplitNavigator()) { return; } if (!hasNewestReportActionRef.current) { @@ -712,7 +712,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 ( (null); /** @@ -32,6 +36,7 @@ function BottomTabBarFloatingActionButton() { return ( diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 890accbd26ac..5f049bda3a6f 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; -import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import {useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,7 +20,7 @@ type SidebarLinksDataProps = { function SidebarLinksData({insets}: SidebarLinksDataProps) { const isFocused = useIsFocused(); const styles = useThemeStyles(); - const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); + const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {initialValue: true}); const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {initialValue: CONST.PRIORITY_MODE.DEFAULT}); @@ -40,7 +40,6 @@ function SidebarLinksData({insets}: SidebarLinksDataProps) { // eslint-disable-next-line react-compiler/react-compiler currentReportIDRef.current = currentReportID; const isActiveReport = useCallback((reportID: string): boolean => currentReportIDRef.current === reportID, []); - return ( { @@ -35,19 +43,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, isLoading]); + }, [activeWorkspace, activeWorkspaceID, isLoading, 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 2e7dc93a116f..4697b1b86b04 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'; @@ -32,9 +32,7 @@ import {canActionTask as canActionTaskUtils, canModifyTask as canModifyTaskUtils import {setSelfTourViewed} from '@libs/actions/Welcome'; 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 {areAllGroupPoliciesExpenseChatDisabled, canSendInvoice as canSendInvoicePolicyUtils, shouldShowPolicy} from '@libs/PolicyUtils'; import {canCreateRequest, generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; @@ -45,21 +43,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 = { @@ -68,6 +56,9 @@ type FloatingActionButtonAndPopoverProps = { /* Callback function before the menu is hidden */ onHideCreateMenu?: () => void; + + /* If the tooltip is allowed to be shown */ + isTooltipAllowed: boolean; }; type FloatingActionButtonAndPopoverRef = { @@ -167,7 +158,7 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { * Responsible for rendering the {@link PopoverMenu}, and the accompanying * FAB that can open or close the menu. */ -function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) { +function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isTooltipAllowed}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -546,6 +537,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl cancelText={translate('common.cancel')} /> - - - ); + 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 3c24f317c812..a478204bb715 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -188,7 +188,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 bc1d5b37c94e..fed2f9091033 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,4 +1,4 @@ -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'; @@ -13,6 +13,8 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {InitialURLContext} from '@components/InitialURLContextProvider'; import MenuItem from '@components/MenuItem'; +import BottomTabBar from '@components/Navigation/BottomTabBar'; +import BOTTOM_TABS from '@components/Navigation/BottomTabBar/BOTTOM_TABS'; import {PressableWithFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; @@ -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,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +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 +98,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); @@ -328,11 +330,15 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr onPress={singleExecution(() => { if (item.action) { item.action(); - } else { - waitForNavigate(() => { - Navigation.navigate(item.routeName); - })(); + return; } + + waitForNavigate(() => { + if (!item.routeName) { + return; + } + Navigation.navigate(item.routeName); + })(); })} iconStyles={item.iconStyles} badgeText={item.badgeText ?? getWalletBalance(isPaymentItem)} @@ -345,11 +351,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 +362,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 +428,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/Security/AddDelegate/ConfirmDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx index 2add4009bb56..18556ddc72d3 100644 --- a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx @@ -76,7 +76,7 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { title={translate('delegate.role', {role})} description={translate('delegate.accessLevel')} helperText={translate('delegate.roleDescription', {role})} - onPress={() => Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(login, role), CONST.NAVIGATION.ACTION_TYPE.PUSH)} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(login, role, ROUTES.SETTINGS_DELEGATE_CONFIRM.getRoute(login, role)))} shouldShowRightIcon /> Navigation.goBack(ROUTES.SETTINGS_ADD_DELEGATE)} + onBackButtonPress={() => Navigation.goBack(route.params?.backTo ?? ROUTES.SETTINGS_ADD_DELEGATE)} /> { 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..2dece9c74df0 100644 --- a/src/pages/settings/Wallet/VerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx @@ -12,7 +12,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as User from '@userActions/User'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -63,7 +62,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { setIsValidateCodeActionModalVisible(false); if (navigateForwardTo) { - Navigation.navigate(navigateForwardTo, CONST.NAVIGATION.TYPE.UP); + Navigation.navigate(navigateForwardTo, {forceReplace: true}); } else { Navigation.goBack(backTo); } diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index b9da0147b525..917ca514d829 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 2a9b77551c0f..f68f00f77846 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -9,6 +9,8 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import HighlightableMenuItem from '@components/HighlightableMenuItem'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; +import BottomTabBar from '@components/Navigation/BottomTabBar'; +import BOTTOM_TABS from '@components/Navigation/BottomTabBar/BOTTOM_TABS'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -23,19 +25,18 @@ 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 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 +71,7 @@ type WorkspaceMenuItem = { badgeText?: string; }; -type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; +type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; type PolicyFeatureStates = Record; @@ -385,27 +386,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} /> @@ -451,7 +448,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac description={translate('workspace.common.workspace')} icon={getIcons(currentUserPolicyExpenseChat, personalDetails)} onPress={() => - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(`${currentUserPolicyExpenseChat?.reportID ?? CONST.DEFAULT_NUMBER_ID}`), CONST.NAVIGATION.TYPE.UP) + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(`${currentUserPolicyExpenseChat?.reportID ?? CONST.DEFAULT_NUMBER_ID}`), {forceReplace: true}) } shouldShowRightIcon wrapperStyle={[styles.br2, styles.pl2, styles.pr0, styles.pv3, styles.mt1, styles.alignItemsCenter]} diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index b9a91fa2ce32..0b167125aa25 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -95,7 +95,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: if (isEmptyObject(policy)) { return; } - Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID, route.params.backTo), true); + Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID, route.params.backTo)); // 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..f1febd9b057e 100644 --- a/src/pages/workspace/WorkspaceJoinUserPage.tsx +++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx @@ -45,7 +45,7 @@ function WorkspaceJoinUserPage({route, policy}: WorkspaceJoinUserPageProps) { } if (!isEmptyObject(policy) && !policy?.isJoinRequestPending && !PolicyUtils.isPendingDeletePolicy(policy)) { Navigation.isNavigationReady().then(() => { - Navigation.goBack(undefined, false, true); + 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 8c3e4152af38..30b6fdeb6a24 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -35,7 +35,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'; @@ -56,7 +56,7 @@ import WorkspacePageWithSections from './WorkspacePageWithSections'; type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & WithCurrentUserPersonalDetailsProps & - PlatformStackScreenProps; + PlatformStackScreenProps; /** * Inverts an object, equivalent of _.invert diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 6c6e70b8cc28..95992b855d7f 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 b8e904c2b78b..dfc841b12919 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -20,10 +20,10 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import resetPolicyIDInNavigationState from '@libs/Navigation/helpers/resetPolicyIDInNavigationState'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList, RootStackParamList, State} 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'; @@ -34,22 +34,21 @@ import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; 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} = usePermissions(); - + const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID}); @@ -169,17 +168,11 @@ 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) { + if (policy.id === activeWorkspaceID) { setActiveWorkspaceID(undefined); - Navigation.dismissModal(); - const rootState = navigationRef.current?.getRootState() as State; - const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState); - if (topmostBottomTabRoute?.name === SCREENS.SETTINGS.ROOT) { - Navigation.setParams({policyID: undefined}, topmostBottomTabRoute?.key); - } + resetPolicyIDInNavigationState(); } - }, [policy?.id, policyName, activeWorkspaceID, setActiveWorkspaceID]); + }, [activeWorkspaceID, policy?.id, policyName, setActiveWorkspaceID]); return ( (); const [policyNameToDelete, setPolicyNameToDelete] = useState(); @@ -153,14 +152,9 @@ 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) { + if (policyIDToDelete === activeWorkspaceID) { setActiveWorkspaceID(undefined); - const rootState = navigationRef.current?.getRootState() as State; - const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState); - if (topmostBottomTabRoute?.name === SCREENS.SETTINGS.ROOT) { - Navigation.setParams({policyID: undefined}, topmostBottomTabRoute?.key); - } + resetPolicyIDInNavigationState(); } }; @@ -432,12 +426,13 @@ function WorkspacesListPage() { shouldEnableMaxHeight testID={WorkspacesListPage.displayName} shouldShowOfflineIndicatorInWideScreen + bottomContent={shouldUseNarrowLayout && } > Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.Buildings} shouldUseHeadlineHeader /> @@ -470,13 +465,14 @@ function WorkspacesListPage() { shouldEnablePickerAvoiding={false} shouldShowOfflineIndicatorInWideScreen testID={WorkspacesListPage.displayName} + bottomContent={shouldUseNarrowLayout && } > Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.Buildings} 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 7b85ddcb02c0..22a570dd03df 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -38,7 +38,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Modal from '@userActions/Modal'; @@ -56,7 +56,7 @@ type PolicyOption = ListItem & { keyForList: string; }; -type WorkspaceCategoriesPageProps = PlatformStackScreenProps; +type WorkspaceCategoriesPageProps = PlatformStackScreenProps; function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 635ff33dbda6..dc24c543e985 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/downgrade/DowngradeIntro.tsx b/src/pages/workspace/downgrade/DowngradeIntro.tsx index d226576d84cb..e26d8c19e22e 100644 --- a/src/pages/workspace/downgrade/DowngradeIntro.tsx +++ b/src/pages/workspace/downgrade/DowngradeIntro.tsx @@ -83,7 +83,7 @@ function DowngradeIntro({onDowngrade, buttonDisabled, loading, policyID}: Props)