diff --git a/src/hooks/useIsWorkspacesTabFocused.ts b/src/hooks/useIsWorkspacesTabFocused.ts new file mode 100644 index 000000000000..708dd7f1d4f7 --- /dev/null +++ b/src/hooks/useIsWorkspacesTabFocused.ts @@ -0,0 +1,21 @@ +import getActiveTabName from '@libs/Navigation/helpers/getActiveTabName'; +import type {NavigationRoute} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import useRootNavigationState from './useRootNavigationState'; + +/** + * Returns true when the Workspaces tab is the active tab in the top-most TAB_NAVIGATOR. + * Stays true when an RHP is pushed on top of a workspace screen, unlike `useIsFocused()` + * which becomes false because the RHP is the leaf focused route. + */ +function useIsWorkspacesTabFocused(): boolean { + return useRootNavigationState((state) => { + if (!state) { + return false; + } + const topTabNavigator = state.routes.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR) as NavigationRoute | undefined; + return getActiveTabName(topTabNavigator) === NAVIGATORS.WORKSPACE_NAVIGATOR; + }); +} + +export default useIsWorkspacesTabFocused; diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 01faa3a0343f..840dc91bae32 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -5,6 +5,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useIsWorkspacesTabFocused from '@hooks/useIsWorkspacesTabFocused'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; @@ -152,6 +153,7 @@ function AccessOrNotFoundWrapper({ const isFromGlobalCreate = !!reportID && isEmptyObject(report?.reportID); const pendingField = featureName ? policy?.pendingFields?.[featureName] : undefined; const isFocused = useIsFocused(); + const isWorkspacesTabFocused = useIsWorkspacesTabFocused(); useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(policy)) { @@ -180,7 +182,10 @@ function AccessOrNotFoundWrapper({ }, true); const isPolicyNotAccessible = !isPolicyAccessible(policy, login); - const shouldShowNotFoundPage = isFocused && ((!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || shouldBeBlocked); + // Gate on `isFocused || isWorkspacesTabFocused` so the fallback renders for both + // - non-workspace consumers of this wrapper (IOU create routes, etc.) where `isFocused` is the meaningful signal, and + // - workspace central-pane usages where an RHP overlay makes `isFocused` false but the Workspaces tab is still active. + const shouldShowNotFoundPage = (isFocused || isWorkspacesTabFocused) && ((!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || shouldBeBlocked); // We only update the feature state if it isn't pending. // This is because the feature state changes several times during the creation of a workspace, while we are waiting for a response from the backend. // Without this, we can be unexpectedly navigated to the More Features page. diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 761c67a7cf46..44e148622921 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -15,6 +15,7 @@ import useCardFeedErrors from '@hooks/useCardFeedErrors'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; +import useIsWorkspacesTabFocused from '@hooks/useIsWorkspacesTabFocused'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -97,6 +98,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const {isBetaEnabled} = usePermissions(); const isFocused = useIsFocused(); + const isWorkspacesTabFocused = useIsWorkspacesTabFocused(); const activeRoute = useNavigationState((state) => findFocusedRoute(state)?.name); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); @@ -190,7 +192,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const prevIsPendingDelete = isPendingDeletePolicy(prevPolicy); // While the policy is being fetched (e.g., right after joinAccessiblePolicy), the role is not yet populated, // so checkIfShouldShowPolicy returns false. Suppress NotFound during this loading window. - const shouldShowNotFoundPage = isFocused && !shouldShowPolicy && !policy?.isLoading && (!isPendingDelete || prevIsPendingDelete); + const shouldShowNotFoundPage = isWorkspacesTabFocused && !shouldShowPolicy && !policy?.isLoading && (!isPendingDelete || prevIsPendingDelete); const fetchPolicyData = () => { if (policyDraft?.id || !isFocused) { return; diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index bfcea2cba5c8..8057b123ec3f 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -11,6 +11,7 @@ import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/typ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; +import useIsWorkspacesTabFocused from '@hooks/useIsWorkspacesTabFocused'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; @@ -151,6 +152,7 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); + const isWorkspacesTabFocused = useIsWorkspacesTabFocused(); const prevPolicy = usePrevious(policy); useEffect(() => { @@ -167,9 +169,10 @@ function WorkspacePageWithSections({ const prevIsPendingDelete = isPendingDeletePolicy(prevPolicy); const shouldShow = useMemo(() => { - // Don't trigger the not-found view while the screen is in the background — prevents unexpected - // navigation when the workspace is deleted from another device while this screen is unfocused. - if (!isFocused) { + // Suppress the not-found view when the user has moved away from the workspace flow (e.g. switched + // to another tab and the workspace was deleted from another device) so the view doesn't bleed + // through over the active tab. Stays true when an RHP is open on top of a workspace screen. + if (!isWorkspacesTabFocused) { return false; } @@ -181,7 +184,7 @@ function WorkspacePageWithSections({ // We check isPendingDelete and prevIsPendingDelete to prevent the NotFound view from showing right after we delete the workspace return (!isEmptyObject(policy) && !canEditWorkspaceSettings(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !(isPendingDelete && !prevIsPendingDelete)); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFocused, policy, shouldShowNonAdmin, shouldShowPolicy]); + }, [isWorkspacesTabFocused, policy, shouldShowNonAdmin, shouldShowPolicy]); const handleOnBackButtonPress = () => { if (shouldShow) {