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

[No QA]: Restricted action: Restrict actions of Delegate Submitter #48485

Merged
merged 25 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3897,6 +3897,7 @@ const CONST = {
SUBMITTER: 'submitter',
ALL: 'all',
},
DELEGATE_ROLE_HELPDOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/',
STRIPE_GBP_AUTH_STATUSES: {
SUCCEEDED: 'succeeded',
CARD_AUTHENTICATION_REQUIRED: 'authentication_required',
Expand Down
39 changes: 39 additions & 0 deletions src/components/DelegateNoAccessModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import useLocalize from '@hooks/useLocalize';
import CONST from '@src/CONST';
import ConfirmModal from './ConfirmModal';
import Text from './Text';
import TextLink from './TextLink';

type DelegateNoAccessModalProps = {
isNoDelegateAccessMenuVisible: boolean;
onConfirm: () => void;
delegatorEmail: string;
};

export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onConfirm, delegatorEmail = ''}: DelegateNoAccessModalProps) {
const {translate} = useLocalize();
const basicnoDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail});
const basicnoDelegateAccessHyperLinked = translate('delegate.notAllowedMessageHyperLinked');
const basicnoDelegateAccessPromptEnd = translate('delegate.notAllowedMessageEnd');
allgandalf marked this conversation as resolved.
Show resolved Hide resolved

const delegateNoAccessPrompt = (
<Text>
{basicnoDelegateAccessPromptStart}
<TextLink href={CONST.DELEGATE_ROLE_HELPDOT_ARTICLE_LINK}>{basicnoDelegateAccessHyperLinked}</TextLink>
{basicnoDelegateAccessPromptEnd}
</Text>
);

return (
<ConfirmModal
isVisible={isNoDelegateAccessMenuVisible}
onConfirm={onConfirm}
onCancel={onConfirm}
allgandalf marked this conversation as resolved.
Show resolved Hide resolved
title={translate('delegate.notAllowed')}
prompt={delegateNoAccessPrompt}
confirmText={translate('common.buttonConfirm')}
shouldShowCancelButton={false}
/>
);
}
18 changes: 16 additions & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import * as TransactionActions from '@userActions/Transaction';
import CONST from '@src/CONST';
import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
Expand All @@ -26,6 +27,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import DelegateNoAccessModal from './DelegateNoAccessModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
Expand Down Expand Up @@ -142,14 +144,18 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID);
const displayedAmount = isAnyTransactionOnHold && canAllowSettlement ? nonHeldAmount : formattedAmount;
const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);

const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type || !chatReport) {
return;
}
setPaymentType(type);
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
if (isAnyTransactionOnHold) {
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true);
} else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
} else if (ReportUtils.isInvoiceReport(moneyRequestReport)) {
IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness);
Expand All @@ -160,7 +166,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea

const confirmApproval = () => {
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
if (isAnyTransactionOnHold) {
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true);
} else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
} else {
IOU.approveMoneyRequest(moneyRequestReport, true);
Expand Down Expand Up @@ -402,6 +410,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
transactionCount={transactionIDs.length}
/>
)}
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onConfirm={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>

<ConfirmModal
title={translate('iou.deleteExpense')}
isVisible={isDeleteRequestModalVisible}
Expand Down
15 changes: 13 additions & 2 deletions src/components/PromotedActionsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ type BasePromotedActions = typeof CONST.PROMOTED_ACTIONS.PIN | typeof CONST.PROM
type PromotedActionsType = Record<BasePromotedActions, (report: OnyxReport) => PromotedAction> & {
message: (params: {reportID?: string; accountID?: number; login?: string}) => PromotedAction;
} & {
hold: (params: {isTextHold: boolean; reportAction: ReportAction | undefined; reportID?: string}) => PromotedAction;
hold: (params: {
isTextHold: boolean;
reportAction: ReportAction | undefined;
reportID?: string;
isDelegateAccessRestricted: boolean;
setIsNoDelegateAccessMenuVisible: (isVisible: boolean) => void;
}) => PromotedAction;
};

const PromotedActions = {
Expand Down Expand Up @@ -70,11 +76,16 @@ const PromotedActions = {
}
},
}),
hold: ({isTextHold, reportAction, reportID}) => ({
hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible}) => ({
key: CONST.PROMOTED_ACTIONS.HOLD,
icon: Expensicons.Stopwatch,
text: Localize.translateLocal(`iou.${isTextHold ? 'hold' : 'unhold'}`),
onSelected: () => {
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true); // Show the menu
return;
}

if (!isTextHold) {
Navigation.goBack();
}
Expand Down
19 changes: 17 additions & 2 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
Expand All @@ -14,6 +15,7 @@ import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
import SettlementButton from '@components/SettlementButton';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
Expand Down Expand Up @@ -191,13 +193,18 @@ function ReportPreview({
[chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);

const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);

const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type) {
return;
}
setPaymentType(type);
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true);
} else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
setIsHoldMenuVisible(true);
} else if (chatReport && iouReport) {
if (ReportUtils.isInvoiceReport(iouReport)) {
Expand All @@ -210,7 +217,9 @@ function ReportPreview({

const confirmApproval = () => {
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true);
} else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
setIsHoldMenuVisible(true);
} else {
IOU.approveMoneyRequest(iouReport, true);
Expand Down Expand Up @@ -516,6 +525,12 @@ function ReportPreview({
</View>
</PressableWithoutFeedback>
</View>
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onConfirm={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>

{isHoldMenuVisible && iouReport && requestType !== undefined && (
<ProcessMoneyReportHoldMenu
nonHeldAmount={!hasOnlyHeldExpenses ? nonHeldAmount : undefined}
Expand Down
18 changes: 18 additions & 0 deletions src/hooks/useDelegateUserDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {useOnyx} from 'react-native-onyx';
import AccountUtils from '@libs/AccountUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';

function useDelegateUserDetails() {
const currentUserDeatils = useCurrentUserPersonalDetails();
allgandalf marked this conversation as resolved.
Show resolved Hide resolved
const [currentUserAccountDetails] = useOnyx(ONYXKEYS.ACCOUNT);
const isDelegateAccessRestricted = AccountUtils.isDelegateOnlySubmitter(currentUserAccountDetails);
const delegatorEmail = currentUserDeatils?.login;

return {
isDelegateAccessRestricted,
delegatorEmail,
};
}

export default useDelegateUserDetails;
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {Country} from '@src/CONST';
import type {DelegateRole} from '@src/types/onyx/Account';
import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
import type {
AccountOwnerParams,
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
Expand Down Expand Up @@ -4570,5 +4571,9 @@ export default {
}
},
genericError: 'Oops, something went wrong. Please try again.',
notAllowed: 'Not so fast...',
notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `You don't have permission to take this action for ${accountOwnerEmail} as a`,
notAllowedMessageHyperLinked: ' limited access',
notAllowedMessageEnd: ' copilot',
},
} satisfies TranslationBase;
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CONST from '@src/CONST';
import type {DelegateRole} from '@src/types/onyx/Account';
import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
import type {
AccountOwnerParams,
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
Expand Down Expand Up @@ -5089,5 +5090,9 @@ export default {
}
},
genericError: '¡Ups! Ha ocurrido un error. Por favor, inténtalo de nuevo.',
notAllowed: 'No tan rápido...',
allgandalf marked this conversation as resolved.
Show resolved Hide resolved
notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `No tienes permiso para realizar esta acción para ${accountOwnerEmail}`,
notAllowedMessageHyperLinked: ' copiloto con acceso',
notAllowedMessageEnd: ' limitado',
},
} satisfies EnglishTranslation;
3 changes: 3 additions & 0 deletions src/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ type ChangeTypeParams = {oldType: string; newType: string};

type DelegateSubmitParams = {delegateUser: string; originalManager: string};

type AccountOwnerParams = {accountOwnerEmail: string};

type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string};

type IntegrationsMessageParams = {
Expand Down Expand Up @@ -475,6 +477,7 @@ export type {
ChangeTypeParams,
ExportedToIntegrationParams,
DelegateSubmitParams,
AccountOwnerParams,
IntegrationsMessageParams,
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
Expand Down
10 changes: 9 additions & 1 deletion src/libs/AccountUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,12 @@ const isValidateCodeFormSubmitting = (account: OnyxEntry<Account>) =>
/** Whether the accound ID is an odd number, useful for A/B testing. */
const isAccountIDOddNumber = (accountID: number) => accountID % 2 === 1;

export default {isValidateCodeFormSubmitting, isAccountIDOddNumber};
function isDelegateOnlySubmitter(account: OnyxEntry<Account>): boolean {
const [delegate] = account?.delegatedAccess?.delegates ?? [];
if (!delegate) {
return false;
}
return delegate?.role === CONST.DELEGATE_ROLE.SUBMITTER;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work if there is more than one delegate? That is, if the delegator has multiple delegates, we need to make sure we're checking if the correct one is a submitter. We should look for the email match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the code now, tested and works fine, thanks for pointing that out @dangrous

Screen.Recording.2024-09-07.at.1.36.30.AM.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh and btw, for the warning prompt, if we do not have the display name of the delegator in case of restricted access, it falls back to the email of the delegator

}

export default {isValidateCodeFormSubmitting, isAccountIDOddNumber, isDelegateOnlySubmitter};
19 changes: 16 additions & 3 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {useOnyx, withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import DisplayNames from '@components/DisplayNames';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
Expand All @@ -22,6 +23,7 @@ import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
Expand Down Expand Up @@ -234,15 +236,19 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD

const [moneyRequestReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`);
const isMoneyRequestExported = ReportUtils.isExported(moneyRequestReportActions);
const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);

const unapproveExpenseReportOrShowModal = useCallback(() => {
if (isMoneyRequestExported) {
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true);
} else if (isMoneyRequestExported) {
setIsUnapproveModalVisible(true);
return;
}
Navigation.dismissModal();
IOU.unapproveExpenseReport(moneyRequestReport);
}, [isMoneyRequestExported, moneyRequestReport]);
}, [isMoneyRequestExported, moneyRequestReport, isDelegateAccessRestricted]);

const shouldShowLeaveButton = ReportUtils.canLeaveChat(report, policy);

Expand Down Expand Up @@ -552,6 +558,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
isTextHold: canHoldUnholdReportAction.canHoldRequest,
reportAction: moneyRequestAction,
reportID: transactionThreadReportID ? report.reportID : moneyRequestAction?.childReportID ?? '-1',
isDelegateAccessRestricted,
setIsNoDelegateAccessMenuVisible,
}),
);
}
Expand All @@ -563,7 +571,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
result.push(PromotedActions.share(report));

return result;
}, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest, transactionThreadReportID]);
}, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest, transactionThreadReportID, isDelegateAccessRestricted]);

const nameSectionExpenseIOU = (
<View style={[styles.reportDetailsRoomInfo, styles.mw100]}>
Expand Down Expand Up @@ -813,6 +821,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
danger
shouldEnableNewFocusManagement
/>
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onConfirm={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>
<ConfirmModal
title={translate('iou.unapproveReport')}
isVisible={isUnapproveModalVisible}
Expand Down
Loading