Skip to content

Commit 9e17f9a

Browse files
authored
Merge pull request Expensify#78300 from mukhrr/fix/copilot-with-limited-access
Add delegate access restriction checks to approval, payment, reject and hold
2 parents ff4c061 + 8ca11e9 commit 9e17f9a

11 files changed

Lines changed: 166 additions & 28 deletions

File tree

src/components/MoneyReportHeader.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,11 @@ function MoneyReportHeader({
14171417
value: CONST.REPORT.SECONDARY_ACTIONS.REJECT,
14181418
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REJECT,
14191419
onSelected: () => {
1420+
if (isDelegateAccessRestricted) {
1421+
showDelegateNoAccessModal();
1422+
return;
1423+
}
1424+
14201425
if (dismissedRejectUseExplanation) {
14211426
if (requestParentReportAction) {
14221427
rejectMoneyRequestReason(requestParentReportAction);

src/components/MoneyRequestHeader.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
440440
icon: Expensicons.ThumbsDown,
441441
value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT,
442442
onSelected: () => {
443+
if (isDelegateAccessRestricted) {
444+
showDelegateNoAccessModal();
445+
return;
446+
}
447+
443448
if (dismissedRejectUseExplanation) {
444449
if (parentReportAction) {
445450
rejectMoneyRequestReason(parentReportAction);

src/components/Search/SearchPageHeader/SearchFiltersBar.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {FlatList, View} from 'react-native';
77
import Button from '@components/Button';
88
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
99
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
10+
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
1011
import KYCWall from '@components/KYCWall';
1112
import {KYCWallContext} from '@components/KYCWall/KYCWallContext';
1213
import type {PaymentMethodType} from '@components/KYCWall/types';
@@ -112,6 +113,7 @@ function SearchFiltersBar({
112113
const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext);
113114
const [searchResultsErrors] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {canBeMissing: true, selector: searchResultsErrorSelector});
114115
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter', 'Columns']);
116+
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
115117

116118
// Get workspace data for the filter
117119
const {sections: workspaces, shouldShowSearchInput: shouldShowWorkspaceSearchInput} = useWorkspaceList({
@@ -850,17 +852,19 @@ function SearchFiltersBar({
850852
customText={selectionButtonText}
851853
options={headerButtonsOptions}
852854
onSubItemSelected={(subItem) =>
853-
handleBulkPayItemSelected(
854-
subItem,
855+
handleBulkPayItemSelected({
856+
item: subItem,
855857
triggerKYCFlow,
856858
isAccountLocked,
857859
showLockedAccountModal,
858-
currentPolicy,
860+
policy: currentPolicy,
859861
latestBankItems,
860862
activeAdminPolicies,
861863
isUserValidated,
864+
isDelegateAccessRestricted,
865+
showDelegateNoAccessModal,
862866
confirmPayment,
863-
)
867+
})
864868
}
865869
isSplitButton={false}
866870
buttonRef={buttonRef}

src/components/SelectionListWithSections/Search/ActionCell.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React, {useCallback} from 'react';
1+
import React, {useCallback, useContext} from 'react';
22
import {View} from 'react-native';
33
import type {ValueOf} from 'type-fest';
44
import Badge from '@components/Badge';
55
import Button from '@components/Button';
6+
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
67
import type {PaymentMethod} from '@components/KYCWall/types';
78
import {SearchScopeProvider} from '@components/Search/SearchScopeProvider';
89
import SettlementButton from '@components/SettlementButton';
@@ -73,6 +74,7 @@ function ActionCell({
7374
const StyleUtils = useStyleUtils();
7475
const {isOffline} = useNetwork();
7576
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Checkbox']);
77+
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
7678
const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID);
7779
const policy = usePolicy(policyID);
7880
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`, {canBeMissing: true});
@@ -88,10 +90,16 @@ function ActionCell({
8890
if (!type || !reportID || !hash || !amount) {
8991
return;
9092
}
93+
94+
if (isDelegateAccessRestricted) {
95+
showDelegateNoAccessModal();
96+
return;
97+
}
98+
9199
const invoiceParams = getPayMoneyOnSearchInvoiceParams(policyID, payAsBusiness, methodID, paymentMethod);
92100
payMoneyRequestOnSearch(hash, [{amount, paymentType: type, reportID, ...(isInvoiceReport(iouReport) ? invoiceParams : {})}]);
93101
},
94-
[reportID, hash, amount, policyID, iouReport],
102+
[reportID, hash, amount, policyID, iouReport, isDelegateAccessRestricted, showDelegateNoAccessModal],
95103
);
96104

97105
if (!isChildListItem && ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE)) {

src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, {useCallback, useMemo} from 'react';
1+
import React, {useCallback, useContext, useMemo} from 'react';
22
import {View} from 'react-native';
3+
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
34
import Icon from '@components/Icon';
45
import {useSearchContext} from '@components/Search/SearchContext';
56
import BaseListItem from '@components/SelectionListWithSections/BaseListItem';
@@ -60,6 +61,8 @@ function ExpenseReportListItem<TItem extends ListItem>({
6061
return isEmpty ?? reportItem.isDisabled ?? reportItem.isDisabledCheckbox;
6162
}, [reportItem.isDisabled, reportItem.isDisabledCheckbox, reportItem.transactions.length]);
6263

64+
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
65+
6366
const handleOnButtonPress = useCallback(() => {
6467
handleActionButtonPress(
6568
currentSearchHash,
@@ -70,8 +73,21 @@ function ExpenseReportListItem<TItem extends ListItem>({
7073
lastPaymentMethod,
7174
currentSearchKey,
7275
onDEWModalOpen,
76+
isDelegateAccessRestricted,
77+
showDelegateNoAccessModal,
7378
);
74-
}, [currentSearchHash, reportItem, onSelectRow, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey, onDEWModalOpen]);
79+
}, [
80+
currentSearchHash,
81+
reportItem,
82+
onSelectRow,
83+
snapshotReport,
84+
snapshotPolicy,
85+
lastPaymentMethod,
86+
currentSearchKey,
87+
onDEWModalOpen,
88+
isDelegateAccessRestricted,
89+
showDelegateNoAccessModal,
90+
]);
7591

7692
const handleCheckboxPress = useCallback(() => {
7793
onCheckboxPress?.(reportItem as unknown as TItem);

src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React, {useMemo} from 'react';
1+
import React, {useContext, useMemo} from 'react';
22
import {View} from 'react-native';
33
import type {ColorValue} from 'react-native';
44
import Checkbox from '@components/Checkbox';
5+
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
56
import Icon from '@components/Icon';
67
import * as Expensicons from '@components/Icon/Expensicons';
78
import {PressableWithFeedback} from '@components/Pressable';
@@ -223,6 +224,7 @@ function ReportListItemHeader<TItem extends ListItem>({
223224
const snapshotPolicy = useMemo(() => {
224225
return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
225226
}, [snapshot, reportItem.policyID]);
227+
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
226228
const avatarBorderColor =
227229
StyleUtils.getItemBackgroundColorStyle(!!reportItem.isSelected, !!isFocused || !!isHovered, !!isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ??
228230
theme.highlightBG;
@@ -237,6 +239,8 @@ function ReportListItemHeader<TItem extends ListItem>({
237239
lastPaymentMethod,
238240
currentSearchKey,
239241
onDEWModalOpen,
242+
isDelegateAccessRestricted,
243+
showDelegateNoAccessModal,
240244
);
241245
};
242246
return !isLargeScreenWidth ? (

src/components/SelectionListWithSections/Search/TransactionListItem.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, {useCallback, useMemo, useRef} from 'react';
1+
import React, {useCallback, useContext, useMemo, useRef} from 'react';
22
import type {View} from 'react-native';
33
import type {OnyxEntry} from 'react-native-onyx';
44
// Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot
55
// eslint-disable-next-line no-restricted-imports
66
import {useOnyx as originalUseOnyx} from 'react-native-onyx';
77
import {getButtonRole} from '@components/Button/utils';
8+
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
89
import OfflineWithFeedback from '@components/OfflineWithFeedback';
910
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
1011
import {useSearchContext} from '@components/Search/SearchContext';
@@ -124,6 +125,8 @@ function TransactionListItem<TItem extends ListItem>({
124125
);
125126
}, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]);
126127

128+
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
129+
127130
const handleActionButtonPress = useCallback(() => {
128131
handleActionButtonPressUtil(
129132
currentSearchHash,
@@ -134,8 +137,23 @@ function TransactionListItem<TItem extends ListItem>({
134137
lastPaymentMethod,
135138
currentSearchKey,
136139
onDEWModalOpen,
140+
isDelegateAccessRestricted,
141+
showDelegateNoAccessModal,
137142
);
138-
}, [currentSearchHash, transactionItem, transactionPreviewData, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey, onSelectRow, item, onDEWModalOpen]);
143+
}, [
144+
currentSearchHash,
145+
transactionItem,
146+
transactionPreviewData,
147+
snapshotReport,
148+
snapshotPolicy,
149+
lastPaymentMethod,
150+
currentSearchKey,
151+
onSelectRow,
152+
item,
153+
onDEWModalOpen,
154+
isDelegateAccessRestricted,
155+
showDelegateNoAccessModal,
156+
]);
139157

140158
const handleCheckboxPress = useCallback(() => {
141159
onCheckboxPress?.(item);

src/components/SettlementButton/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {GestureResponderEvent} from 'react-native';
66
import type {TupleToUnion} from 'type-fest';
77
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
88
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
9+
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
910
import KYCWall from '@components/KYCWall';
1011
import {KYCWallContext} from '@components/KYCWall/KYCWallContext';
1112
import type {ContinueActionParams, PaymentMethod} from '@components/KYCWall/types';
@@ -154,6 +155,7 @@ function SettlementButton({
154155
const isInvoiceReport = (!isEmptyObject(iouReport) && isInvoiceReportUtil(iouReport)) || false;
155156

156157
const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext);
158+
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
157159
const kycWallRef = useContext(KYCWallContext);
158160
const shouldShowPayWithExpensifyOption = !shouldHidePaymentOptions;
159161
const shouldShowPayElsewhereOption = !shouldHidePaymentOptions && !isInvoiceReport;
@@ -185,6 +187,11 @@ function SettlementButton({
185187
}
186188

187189
const checkForNecessaryAction = useCallback(() => {
190+
if (isDelegateAccessRestricted) {
191+
showDelegateNoAccessModal();
192+
return true;
193+
}
194+
188195
if (isAccountLocked) {
189196
showLockedAccountModal();
190197
return true;
@@ -201,7 +208,7 @@ function SettlementButton({
201208
}
202209

203210
return false;
204-
}, [policy, isAccountLocked, isUserValidated, chatReportID, reportID, showLockedAccountModal]);
211+
}, [policy, isAccountLocked, isUserValidated, chatReportID, reportID, showLockedAccountModal, isDelegateAccessRestricted, showDelegateNoAccessModal]);
205212

206213
const getPaymentSubitems = useCallback(
207214
(payAsBusiness: boolean) => {

src/libs/actions/Search.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ function handleActionButtonPress(
8181
lastPaymentMethod: OnyxEntry<LastPaymentMethod>,
8282
currentSearchKey?: SearchKey,
8383
onDEWModalOpen?: () => void,
84+
isDelegateAccessRestricted?: boolean,
85+
onDelegateAccessRestricted?: () => void,
8486
) {
8587
// The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid.
8688
// We need the transactionID to display the loading indicator for that list item's action.
@@ -95,9 +97,17 @@ function handleActionButtonPress(
9597

9698
switch (item.action) {
9799
case CONST.SEARCH.ACTION_TYPES.PAY:
100+
if (isDelegateAccessRestricted) {
101+
onDelegateAccessRestricted?.();
102+
return;
103+
}
98104
getPayActionCallback(hash, item, goToItem, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey);
99105
return;
100106
case CONST.SEARCH.ACTION_TYPES.APPROVE:
107+
if (isDelegateAccessRestricted) {
108+
onDelegateAccessRestricted?.();
109+
return;
110+
}
101111
if (hasDynamicExternalWorkflow(snapshotPolicy)) {
102112
onDEWModalOpen?.();
103113
return;
@@ -1054,22 +1064,43 @@ function isValidBulkPayOption(item: PopoverMenuItem) {
10541064
/**
10551065
* Handles the click event when user selects bulk pay action.
10561066
*/
1057-
function handleBulkPayItemSelected(
1058-
item: PopoverMenuItem,
1059-
triggerKYCFlow: (params: ContinueActionParams) => void,
1060-
isAccountLocked: boolean,
1061-
showLockedAccountModal: () => void,
1062-
policy: OnyxEntry<Policy>,
1063-
latestBankItems: BankAccountMenuItem[] | undefined,
1064-
activeAdminPolicies: Policy[],
1065-
isUserValidated: boolean | undefined,
1066-
confirmPayment?: (paymentType: PaymentMethodType | undefined, additionalData?: Record<string, unknown>) => void,
1067-
) {
1067+
function handleBulkPayItemSelected(params: {
1068+
item: PopoverMenuItem;
1069+
triggerKYCFlow: (params: ContinueActionParams) => void;
1070+
isAccountLocked: boolean;
1071+
showLockedAccountModal: () => void;
1072+
policy: OnyxEntry<Policy>;
1073+
latestBankItems: BankAccountMenuItem[] | undefined;
1074+
activeAdminPolicies: Policy[];
1075+
isUserValidated: boolean | undefined;
1076+
isDelegateAccessRestricted: boolean;
1077+
showDelegateNoAccessModal: () => void;
1078+
confirmPayment?: (paymentType: PaymentMethodType | undefined, additionalData?: Record<string, unknown>) => void;
1079+
}) {
1080+
const {
1081+
item,
1082+
triggerKYCFlow,
1083+
isAccountLocked,
1084+
showLockedAccountModal,
1085+
policy,
1086+
latestBankItems,
1087+
activeAdminPolicies,
1088+
isUserValidated,
1089+
isDelegateAccessRestricted,
1090+
showDelegateNoAccessModal,
1091+
confirmPayment,
1092+
} = params;
10681093
const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(item.key, activeAdminPolicies, latestBankItems);
10691094
// Policy id is also a last payment method so we shouldn't early return here for that case.
10701095
if (!isValidBulkPayOption(item) && !selectedPolicy) {
10711096
return;
10721097
}
1098+
1099+
if (isDelegateAccessRestricted) {
1100+
showDelegateNoAccessModal();
1101+
return;
1102+
}
1103+
10731104
if (isAccountLocked) {
10741105
showLockedAccountModal();
10751106
return;

0 commit comments

Comments
 (0)