diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7a284ee527b9..6662016201c7 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -210,17 +210,17 @@ function getPolicyRole(policy: OnyxInputOrEntry | SearchPolicy, currentU /** * Check if the policy can be displayed - * If offline, always show the policy pending deletion. - * If online, show the policy pending deletion only if there is an error. + * If shouldShowPendingDeletePolicy is true, show the policy pending deletion. + * If shouldShowPendingDeletePolicy is false, show the policy pending deletion only if there is an error. * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. */ -function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean, currentUserLogin: string | undefined): boolean { +function shouldShowPolicy(policy: OnyxEntry, shouldShowPendingDeletePolicy: boolean, currentUserLogin: string | undefined): boolean { return ( !!policy?.isJoinRequestPending || (!!policy && policy?.type !== CONST.POLICY.TYPE.PERSONAL && - (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) && + (shouldShowPendingDeletePolicy || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) && !!getPolicyRole(policy, currentUserLogin)) ); } @@ -1165,7 +1165,9 @@ function getUserFriendlyWorkspaceType(workspaceType: ValueOf): boolean { - return !isEmptyObject(policy) && (Object.keys(policy).length !== 1 || isEmptyObject(policy.errors)) && !!policy?.id; + return ( + !isEmptyObject(policy) && (Object.keys(policy).length !== 1 || isEmptyObject(policy.errors)) && !!policy?.id && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ); } function areAllGroupPoliciesExpenseChatDisabled(policies = allPolicies) { diff --git a/src/pages/ErrorPage/NotFoundPage.tsx b/src/pages/ErrorPage/NotFoundPage.tsx index 26b6aae5bc0b..51149e0ac775 100644 --- a/src/pages/ErrorPage/NotFoundPage.tsx +++ b/src/pages/ErrorPage/NotFoundPage.tsx @@ -10,10 +10,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; type NotFoundPageProps = { onBackButtonPress?: () => void; isReportRelatedPage?: boolean; + shouldShowOfflineIndicator?: boolean; } & FullPageNotFoundViewProps; // eslint-disable-next-line rulesdir/no-negated-variables -function NotFoundPage({onBackButtonPress = () => Navigation.goBack(), isReportRelatedPage, ...fullPageNotFoundViewProps}: NotFoundPageProps) { +function NotFoundPage({onBackButtonPress = () => Navigation.goBack(), isReportRelatedPage, shouldShowOfflineIndicator, ...fullPageNotFoundViewProps}: NotFoundPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to go back to the not found page on large screens and to the home page on small screen // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); @@ -21,7 +22,10 @@ function NotFoundPage({onBackButtonPress = () => Navigation.goBack(), isReportRe const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${topmostReportId}`); return ( - + { diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index b9318ee42664..35c89cc72285 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -21,7 +21,7 @@ import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp, PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReimbursementAccountNavigatorParamList} from '@libs/Navigation/types'; -import {goBackFromInvalidPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import {getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; import shouldReopenOnfido from '@libs/shouldReopenOnfido'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; @@ -417,14 +417,14 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return ; } - if (!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) { + if ((!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) || isPendingDeletePolicy(policy)) { return ( ); diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index b9da0147b525..db33902c2d88 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -7,50 +7,42 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import * as IOUUtils from '@libs/IOUUtils'; +import {openWorkspace} from '@libs/actions/Policy/Policy'; +import {isValidMoneyRequestType} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {canSendInvoice, isControlPolicy, isPaidGroupPolicy, isPolicyAccessible, isPolicyAdmin, isPolicyFeatureEnabled as isPolicyFeatureEnabledUtil} from '@libs/PolicyUtils'; +import {canCreateRequest} from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import * as Policy from '@userActions/Policy/Policy'; import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; +import type {Report} from '@src/types/onyx'; import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import type Policy from '@src/types/onyx/Policy'; import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const ACCESS_VARIANTS = { - [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy), - [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => PolicyUtils.isControlPolicy(policy), - [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login), - [CONST.IOU.ACCESS_VARIANTS.CREATE]: ( - policy: OnyxEntry, - login: string, - report: OnyxEntry, - allPolicies: NonNullable> | null, - iouType?: IOUType, - ) => + [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => isPaidGroupPolicy(policy), + [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => isControlPolicy(policy), + [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => isPolicyAdmin(policy, login), + [CONST.IOU.ACCESS_VARIANTS.CREATE]: (policy: OnyxEntry, login: string, report: OnyxEntry, allPolicies: NonNullable> | null, iouType?: IOUType) => !!iouType && - IOUUtils.isValidMoneyRequestType(iouType) && + isValidMoneyRequestType(iouType) && // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the expense - (isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType)) && - (iouType !== CONST.IOU.TYPE.INVOICE || PolicyUtils.canSendInvoice(allPolicies, login)), -} as const satisfies Record< - string, - (policy: OnyxTypes.Policy, login: string, report: OnyxTypes.Report, allPolicies: NonNullable> | null, iouType?: IOUType) => boolean ->; + (isEmptyObject(report?.reportID) || canCreateRequest(report, policy, iouType)) && + (iouType !== CONST.IOU.TYPE.INVOICE || canSendInvoice(allPolicies, login)), +} as const satisfies Record> | null, iouType?: IOUType) => boolean>; type AccessVariant = keyof typeof ACCESS_VARIANTS; type AccessOrNotFoundWrapperChildrenProps = { /** The report that holds the transaction */ - report: OnyxEntry; + report: OnyxEntry; /** The report currently being looked at */ - policy: OnyxEntry; + policy: OnyxEntry; /** Indicated whether the report data is loading */ isLoadingReportData: OnyxEntry; @@ -82,7 +74,7 @@ type AccessOrNotFoundWrapperProps = { iouType?: IOUType; /** The list of all policies */ - allPolicies?: OnyxCollection; + allPolicies?: OnyxCollection; } & Pick; type PageNotFoundFallbackProps = Pick & { @@ -97,6 +89,7 @@ function PageNotFoundFallback({policyID, fullPageNotFoundViewProps, isFeatureEna return ( { if (isPolicyNotAccessible) { Navigation.dismissModal(); @@ -127,8 +120,8 @@ function AccessOrNotFoundWrapper({ const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); const {login = ''} = useCurrentUserPersonalDetails(); const isPolicyIDInRoute = !!policyID?.length; - const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType); - const isFromGlobalCreate = isEmptyObject(report?.reportID); + const isMoneyRequest = !!iouType && isValidMoneyRequestType(iouType); + const isFromGlobalCreate = !!reportID && isEmptyObject(report?.reportID); const pendingField = featureName ? policy?.pendingFields?.[featureName] : undefined; useEffect(() => { @@ -137,13 +130,13 @@ function AccessOrNotFoundWrapper({ return; } - Policy.openWorkspace(policyID, []); + openWorkspace(policyID, []); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isPolicyIDInRoute, policyID]); const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); - const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true; + const isFeatureEnabled = featureName ? isPolicyFeatureEnabledUtil(policy, featureName) : true; const [isPolicyFeatureEnabled, setIsPolicyFeatureEnabled] = useState(isFeatureEnabled); const {isOffline} = useNetwork(); @@ -153,9 +146,8 @@ function AccessOrNotFoundWrapper({ return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType); }, true); - const isPolicyNotAccessible = !PolicyUtils.isPolicyAccessible(policy); + const isPolicyNotAccessible = !isPolicyAccessible(policy); const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isPolicyFeatureEnabled || 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 have unexpectedly have 'Not Found' be shown. diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index c51154df40d6..af7d71b70d92 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -21,6 +21,8 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {isConnectionInProgress} from '@libs/actions/connections'; +import {clearErrors, openPolicyInitialPage, removeWorkspace, updateGeneralSettings} from '@libs/actions/Policy/Policy'; +import {navigateToBankAccountRoute} from '@libs/actions/ReimbursementAccount'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; @@ -42,8 +44,6 @@ import {getDefaultWorkspaceAvatar, getIcons, getPolicyExpenseChat, getReportName import type {FullScreenNavigatorParamList} from '@navigation/types'; import {confirmReadyToOpenApp} from '@userActions/App'; import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@userActions/CompanyCards'; -import {clearErrors, openPolicyInitialPage, removeWorkspace, updateGeneralSettings} from '@userActions/Policy/Policy'; -import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -353,8 +353,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); - const shouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); + const shouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(policy, false, currentUserLogin), [policy, currentUserLogin]); + const prevShouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(prevPolicy, false, currentUserLogin), [prevPolicy, currentUserLogin]); // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 26175c9793d9..8867e32fa8ef 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -10,14 +10,13 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/types'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; -import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {openWorkspaceView} from '@libs/actions/BankAccounts'; import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as BankAccounts from '@userActions/BankAccounts'; +import {isPolicyAdmin, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -92,7 +91,7 @@ function fetchData(policyID: string, skipVBBACal?: boolean) { return; } - BankAccounts.openWorkspaceView(policyID); + openWorkspaceView(policyID); } function WorkspacePageWithSections({ @@ -123,8 +122,7 @@ function WorkspacePageWithSections({ threeDotsAnchorPosition, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); - const policyID = route.params?.policyID ?? '-1'; - const {isOffline} = useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); + const policyID = route.params?.policyID ?? `${CONST.DEFAULT_NUMBER_ID}`; const [user] = useOnyx(ONYXKEYS.USER); const [reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -132,7 +130,7 @@ function WorkspacePageWithSections({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; - const achState = reimbursementAccount?.achData?.state ?? '-1'; + const achState = reimbursementAccount?.achData?.state; const isUsingECard = user?.isUsingExpensifyCard ?? false; const hasVBA = achState === BankAccount.STATE.OPEN; const content = typeof children === 'function' ? children(hasVBA, policyID, isUsingECard) : children; @@ -151,8 +149,8 @@ function WorkspacePageWithSections({ }, [policyID, shouldSkipVBBACall]), ); - const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); + const shouldShowPolicy = useMemo(() => shouldShowPolicyUtil(policy, false, currentUserLogin), [policy, currentUserLogin]); + const prevShouldShowPolicy = useMemo(() => shouldShowPolicyUtil(prevPolicy, false, currentUserLogin), [prevPolicy, currentUserLogin]); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { @@ -160,7 +158,7 @@ function WorkspacePageWithSections({ } // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace - return (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy); + return (!isEmptyObject(policy) && !isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [policy, shouldShowNonAdmin, shouldShowPolicy, prevShouldShowPolicy]); @@ -170,6 +168,7 @@ function WorkspacePageWithSections({ shouldEnablePickerAvoiding={false} shouldEnableMaxHeight testID={testID ?? WorkspacePageWithSections.displayName} + shouldShowOfflineIndicator={!shouldShow} shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} >