Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix - Workspace - User can navigate to deleted workspace editor while offline #55579

12 changes: 7 additions & 5 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,17 +210,17 @@ function getPolicyRole(policy: OnyxInputOrEntry<Policy> | 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<Policy>, isOffline: boolean, currentUserLogin: string | undefined): boolean {
function shouldShowPolicy(policy: OnyxEntry<Policy>, 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))
);
}
Expand Down Expand Up @@ -1165,7 +1165,9 @@ function getUserFriendlyWorkspaceType(workspaceType: ValueOf<typeof CONST.POLICY
}

function isPolicyAccessible(policy: OnyxEntry<Policy>): 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) {
Expand Down
8 changes: 6 additions & 2 deletions src/pages/ErrorPage/NotFoundPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ 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();
const topmostReportId = Navigation.getTopmostReportId();
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${topmostReportId}`);

return (
<ScreenWrapper testID={NotFoundPage.displayName}>
<ScreenWrapper
testID={NotFoundPage.displayName}
shouldShowOfflineIndicator={shouldShowOfflineIndicator}
>
<FullPageNotFoundView
shouldShow
onBackButtonPress={() => {
Expand Down
6 changes: 3 additions & 3 deletions src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -417,14 +417,14 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen
return <ReimbursementAccountLoadingIndicator onBackButtonPress={goBack} />;
}

if (!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) {
if ((!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) || isPendingDeletePolicy(policy)) {
return (
<ScreenWrapper testID={ReimbursementAccountPage.displayName}>
<FullPageNotFoundView
shouldShow
onBackButtonPress={goBackFromInvalidPolicy}
onLinkPress={goBackFromInvalidPolicy}
subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}
subtitleKey={isEmptyObject(policy) || isPendingDeletePolicy(policy) ? undefined : 'workspace.common.notAuthorized'}
/>
</ScreenWrapper>
);
Expand Down
54 changes: 23 additions & 31 deletions src/pages/workspace/AccessOrNotFoundWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<OnyxTypes.Policy>) => PolicyUtils.isPaidGroupPolicy(policy),
[CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isControlPolicy(policy),
[CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry<OnyxTypes.Policy>, login: string) => PolicyUtils.isPolicyAdmin(policy, login),
[CONST.IOU.ACCESS_VARIANTS.CREATE]: (
policy: OnyxEntry<OnyxTypes.Policy>,
login: string,
report: OnyxEntry<OnyxTypes.Report>,
allPolicies: NonNullable<OnyxCollection<OnyxTypes.Policy>> | null,
iouType?: IOUType,
) =>
[CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry<Policy>) => isPaidGroupPolicy(policy),
[CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry<Policy>) => isControlPolicy(policy),
[CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry<Policy>, login: string) => isPolicyAdmin(policy, login),
[CONST.IOU.ACCESS_VARIANTS.CREATE]: (policy: OnyxEntry<Policy>, login: string, report: OnyxEntry<Report>, allPolicies: NonNullable<OnyxCollection<Policy>> | 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<OnyxCollection<OnyxTypes.Policy>> | null, iouType?: IOUType) => boolean
>;
(isEmptyObject(report?.reportID) || canCreateRequest(report, policy, iouType)) &&
(iouType !== CONST.IOU.TYPE.INVOICE || canSendInvoice(allPolicies, login)),
} as const satisfies Record<string, (policy: Policy, login: string, report: Report, allPolicies: NonNullable<OnyxCollection<Policy>> | null, iouType?: IOUType) => boolean>;

type AccessVariant = keyof typeof ACCESS_VARIANTS;
type AccessOrNotFoundWrapperChildrenProps = {
/** The report that holds the transaction */
report: OnyxEntry<OnyxTypes.Report>;
report: OnyxEntry<Report>;

/** The report currently being looked at */
policy: OnyxEntry<OnyxTypes.Policy>;
policy: OnyxEntry<Policy>;

/** Indicated whether the report data is loading */
isLoadingReportData: OnyxEntry<boolean>;
Expand Down Expand Up @@ -82,7 +74,7 @@ type AccessOrNotFoundWrapperProps = {
iouType?: IOUType;

/** The list of all policies */
allPolicies?: OnyxCollection<OnyxTypes.Policy>;
allPolicies?: OnyxCollection<Policy>;
} & Pick<FullPageNotFoundViewProps, 'subtitleKey' | 'onLinkPress'>;

type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID' | 'fullPageNotFoundViewProps'> & {
Expand All @@ -97,6 +89,7 @@ function PageNotFoundFallback({policyID, fullPageNotFoundViewProps, isFeatureEna
return (
<NotFoundPage
shouldForceFullScreen={shouldShowFullScreenFallback}
shouldShowOfflineIndicator={false}
onBackButtonPress={() => {
if (isPolicyNotAccessible) {
Navigation.dismissModal();
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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();
Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/pages/workspace/WorkspaceInitialPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 9 additions & 10 deletions src/pages/workspace/WorkspacePageWithSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -92,7 +91,7 @@ function fetchData(policyID: string, skipVBBACal?: boolean) {
return;
}

BankAccounts.openWorkspaceView(policyID);
openWorkspaceView(policyID);
}

function WorkspacePageWithSections({
Expand Down Expand Up @@ -123,16 +122,15 @@ 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);
const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});

// 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;
Expand All @@ -151,16 +149,16 @@ 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) {
return true;
}

// 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]);

Expand All @@ -170,6 +168,7 @@ function WorkspacePageWithSections({
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
testID={testID ?? WorkspacePageWithSections.displayName}
shouldShowOfflineIndicator={!shouldShow}
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
>
<FullPageNotFoundView
Expand Down
Loading