Skip to content

Commit

Permalink
Merge pull request #41730 from tienifr/fix/41305
Browse files Browse the repository at this point in the history
Access control on IOU request
  • Loading branch information
cristipaval authored May 30, 2024
2 parents 8a18d87 + a72e59b commit ea41f1d
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 37 deletions.
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,9 @@ const CONST = {
ACCOUNTANT: 'accountant',
},
},
ACCESS_VARIANTS: {
CREATE: 'create',
},
},

GROWL: {
Expand Down
4 changes: 2 additions & 2 deletions src/components/ConnectionLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
Expand Down Expand Up @@ -33,7 +33,7 @@ type ConnectionLayoutProps = {
policyID: string;

/** Defines which types of access should be verified */
accessVariants?: PolicyAccessVariant[];
accessVariants?: AccessVariant[];

/** The current feature name that the user tries to get access to */
featureName?: PolicyFeatureName;
Expand Down
4 changes: 2 additions & 2 deletions src/components/SelectionScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import useLocalize from '@hooks/useLocalize';
import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
Expand Down Expand Up @@ -45,7 +45,7 @@ type SelectionScreenProps = {
policyID: string;

/** Defines which types of access should be verified */
accessVariants?: PolicyAccessVariant[];
accessVariants?: AccessVariant[];

/** The current feature name that the user tries to get access to */
featureName?: PolicyFeatureName;
Expand Down
35 changes: 18 additions & 17 deletions src/pages/iou/request/IOURequestStartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import DragAndDropProvider from '@components/DragAndDrop/Provider';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
Expand All @@ -13,13 +12,12 @@ import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as IOUUtils from '@libs/IOUUtils';
import * as KeyDownPressListener from '@libs/KeyboardShortcut/KeyDownPressListener';
import Navigation from '@libs/Navigation/Navigation';
import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import * as IOU from '@userActions/IOU';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -105,9 +103,6 @@ function IOURequestStartPage({
const isExpenseReport = ReportUtils.isExpenseReport(report);
const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT;

// Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense
const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies);

const navigateBack = () => {
Navigation.closeRHPFlow();
};
Expand All @@ -126,15 +121,21 @@ function IOURequestStartPage({
}

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableKeyboardAvoidingView={false}
shouldEnableMinHeight={DeviceCapabilities.canUseTouchScreen()}
headerGapStyles={isDraggingOver ? [styles.receiptDropHeaderGap] : []}
testID={IOURequestStartPage.displayName}
<AccessOrNotFoundWrapper
reportID={reportID}
iouType={iouType}
policyID={policy?.id}
accessVariants={[CONST.IOU.ACCESS_VARIANTS.CREATE]}
allPolicies={allPolicies}
>
{({safeAreaPaddingBottomStyle}) => (
<FullPageNotFoundView shouldShow={!IOUUtils.isValidMoneyRequestType(iouType) || !isAllowedToCreateRequest}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableKeyboardAvoidingView={false}
shouldEnableMinHeight={DeviceCapabilities.canUseTouchScreen()}
headerGapStyles={isDraggingOver ? [styles.receiptDropHeaderGap] : []}
testID={IOURequestStartPage.displayName}
>
{({safeAreaPaddingBottomStyle}) => (
<DragAndDropProvider
setIsDraggingOver={setIsDraggingOver}
isDisabled={selectedTab !== CONST.TAB_REQUEST.SCAN}
Expand All @@ -159,9 +160,9 @@ function IOURequestStartPage({
)}
</View>
</DragAndDropProvider>
</FullPageNotFoundView>
)}
</ScreenWrapper>
)}
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

Expand Down
58 changes: 42 additions & 16 deletions src/pages/workspace/AccessOrNotFoundWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/* eslint-disable rulesdir/no-negated-variables */
import React, {useEffect} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils 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';
Expand All @@ -17,13 +20,22 @@ import type {PolicyFeatureName} from '@src/types/onyx/Policy';
import callOrReturn from '@src/types/utils/callOrReturn';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

const POLICY_ACCESS_VARIANTS = {
const ACCESS_VARIANTS = {
[CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPaidGroupPolicy(policy) && !!policy?.isPolicyExpenseChatEnabled,
[CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPolicyAdmin(policy),
} as const satisfies Record<string, (policy: OnyxTypes.Policy) => boolean>;

type PolicyAccessVariant = keyof typeof POLICY_ACCESS_VARIANTS;
[CONST.IOU.ACCESS_VARIANTS.CREATE]: (policy: OnyxEntry<OnyxTypes.Policy>, report: OnyxEntry<OnyxTypes.Report>, allPolicies: OnyxCollection<OnyxTypes.Policy>, iouType?: IOUType) =>
!!iouType &&
IOUUtils.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)),
} as const satisfies Record<string, (policy: OnyxTypes.Policy, report: OnyxTypes.Report, allPolicies: OnyxCollection<OnyxTypes.Policy>, iouType?: IOUType) => boolean>;

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

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

Expand All @@ -35,11 +47,14 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & {
/** The children to render */
children: ((props: AccessOrNotFoundWrapperOnyxProps) => React.ReactNode) | React.ReactNode;

/** The id of the report that holds the transaction */
reportID?: string;

/** The report currently being looked at */
policyID: string;
policyID?: string;

/** Defines which types of access should be verified */
accessVariants?: PolicyAccessVariant[];
accessVariants?: AccessVariant[];

/** The current feature name that the user tries to get access to */
featureName?: PolicyFeatureName;
Expand All @@ -49,6 +64,12 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & {

/** Whether or not to block user from accessing the page */
shouldBeBlocked?: boolean;

/** The type of the transaction */
iouType?: IOUType;

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

type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID' | 'fullPageNotFoundViewProps'> & {shouldShowFullScreenFallback: boolean};
Expand All @@ -64,17 +85,19 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN
/>
) : (
<NotFoundPage
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))}
onBackButtonPress={() => Navigation.goBack(policyID ? ROUTES.WORKSPACE_PROFILE.getRoute(policyID) : ROUTES.HOME)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...fullPageNotFoundViewProps}
/>
);
}

function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, shouldBeBlocked, ...props}: AccessOrNotFoundWrapperProps) {
const {policy, policyID, featureName, isLoadingReportData} = props;
const {policy, policyID, report, iouType, allPolicies, featureName, isLoadingReportData} = props;

const isPolicyIDInRoute = !!policyID?.length;
const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType);
const isFromGlobalCreate = isEmptyObject(report?.reportID);

useEffect(() => {
if (!isPolicyIDInRoute || !isEmptyObject(policy)) {
Expand All @@ -86,17 +109,17 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPolicyIDInRoute, policyID]);

const shouldShowFullScreenLoadingIndicator = isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id);
const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id);

const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true;

const isPageAccessible = accessVariants.reduce((acc, variant) => {
const accessFunction = POLICY_ACCESS_VARIANTS[variant];
return acc && accessFunction(policy);
const accessFunction = ACCESS_VARIANTS[variant];
return acc && accessFunction(policy, report, allPolicies ?? null, iouType);
}, true);

const shouldShowNotFoundPage =
isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked;
const isPolicyNotAccessible = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id;
const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked;

if (shouldShowFullScreenLoadingIndicator) {
return <FullscreenLoadingIndicator />;
Expand All @@ -115,11 +138,14 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
return callOrReturn(props.children, props);
}

export type {PolicyAccessVariant};
export type {AccessVariant};

export default withOnyx<AccessOrNotFoundWrapperProps, AccessOrNotFoundWrapperOnyxProps>({
report: {
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
},
policy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`,
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
},
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
Expand Down

0 comments on commit ea41f1d

Please sign in to comment.