diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 3d0cb8c079119..3f415cd3f5609 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1281,6 +1281,11 @@ function MoneyReportHeader({ icon: expensifyIcons.ThumbsDown, value: CONST.REPORT.SECONDARY_ACTIONS.REJECT, onSelected: () => { + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + if (dismissedRejectUseExplanation) { if (requestParentReportAction) { rejectMoneyRequestReason(requestParentReportAction); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 148e32a13dfe6..127c40bff9ab5 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -372,6 +372,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre icon: Expensicons.ThumbsDown, value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT, onSelected: () => { + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + if (dismissedRejectUseExplanation) { if (parentReportAction) { rejectMoneyRequestReason(parentReportAction); diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 7617f7f794160..f0c202d3408eb 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -8,6 +8,7 @@ import {FlatList, View} from 'react-native'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import KYCWall from '@components/KYCWall'; import {KYCWallContext} from '@components/KYCWall/KYCWallContext'; import type {PaymentMethodType} from '@components/KYCWall/types'; @@ -113,6 +114,7 @@ function SearchFiltersBar({ const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); const [searchResultsErrors] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {canBeMissing: true, selector: searchResultsErrorSelector}); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter'] as const); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const taxRates = getAllTaxRates(allPolicies); @@ -811,17 +813,19 @@ function SearchFiltersBar({ customText={selectionButtonText} options={headerButtonsOptions} onSubItemSelected={(subItem) => - handleBulkPayItemSelected( - subItem, + handleBulkPayItemSelected({ + item: subItem, triggerKYCFlow, isAccountLocked, showLockedAccountModal, - currentPolicy, + policy: currentPolicy, latestBankItems, activeAdminPolicies, isUserValidated, + isDelegateAccessRestricted, + showDelegateNoAccessModal, confirmPayment, - ) + }) } isSplitButton={false} buttonRef={buttonRef} diff --git a/src/components/SelectionListWithSections/Search/ActionCell.tsx b/src/components/SelectionListWithSections/Search/ActionCell.tsx index 6cedd17335941..7cacbcd904680 100644 --- a/src/components/SelectionListWithSections/Search/ActionCell.tsx +++ b/src/components/SelectionListWithSections/Search/ActionCell.tsx @@ -1,8 +1,9 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useContext} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import Badge from '@components/Badge'; import Button from '@components/Button'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import type {PaymentMethod} from '@components/KYCWall/types'; import {SearchScopeProvider} from '@components/Search/SearchScopeProvider'; import SettlementButton from '@components/SettlementButton'; @@ -72,6 +73,7 @@ function ActionCell({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Checkbox'] as const); const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID); const policy = usePolicy(policyID); @@ -88,10 +90,16 @@ function ActionCell({ if (!type || !reportID || !hash || !amount) { return; } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const invoiceParams = getPayMoneyOnSearchInvoiceParams(policyID, payAsBusiness, methodID, paymentMethod); payMoneyRequestOnSearch(hash, [{amount, paymentType: type, reportID, ...(isInvoiceReport(iouReport) ? invoiceParams : {})}]); }, - [reportID, hash, amount, policyID, iouReport], + [reportID, hash, amount, policyID, iouReport, isDelegateAccessRestricted, showDelegateNoAccessModal], ); if (!isChildListItem && ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE)) { diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx index ecfb4169c1a6d..2d36a161f7a3c 100644 --- a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx +++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx @@ -1,5 +1,6 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useContext, useMemo} from 'react'; import {View} from 'react-native'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import Icon from '@components/Icon'; import {useSearchContext} from '@components/Search/SearchContext'; import BaseListItem from '@components/SelectionListWithSections/BaseListItem'; @@ -59,6 +60,8 @@ function ExpenseReportListItem({ return isEmpty ?? reportItem.isDisabled ?? reportItem.isDisabledCheckbox; }, [reportItem.isDisabled, reportItem.isDisabledCheckbox, reportItem.transactions.length]); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); + const handleOnButtonPress = useCallback(() => { handleActionButtonPress( currentSearchHash, @@ -69,8 +72,21 @@ function ExpenseReportListItem({ lastPaymentMethod, currentSearchKey, onDEWModalOpen, + isDelegateAccessRestricted, + showDelegateNoAccessModal, ); - }, [currentSearchHash, reportItem, onSelectRow, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey, onDEWModalOpen]); + }, [ + currentSearchHash, + reportItem, + onSelectRow, + snapshotReport, + snapshotPolicy, + lastPaymentMethod, + currentSearchKey, + onDEWModalOpen, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + ]); const handleCheckboxPress = useCallback(() => { onCheckboxPress?.(reportItem as unknown as TItem); diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx index 409c422e155f9..f93076f1b1a73 100644 --- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx @@ -1,7 +1,8 @@ -import React, {useMemo} from 'react'; +import React, {useContext, useMemo} from 'react'; import {View} from 'react-native'; import type {ColorValue} from 'react-native'; import Checkbox from '@components/Checkbox'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithFeedback} from '@components/Pressable'; @@ -223,6 +224,7 @@ function ReportListItemHeader({ const snapshotPolicy = useMemo(() => { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy; }, [snapshot, reportItem.policyID]); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const avatarBorderColor = StyleUtils.getItemBackgroundColorStyle(!!reportItem.isSelected, !!isFocused || !!isHovered, !!isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ?? theme.highlightBG; @@ -237,6 +239,8 @@ function ReportListItemHeader({ lastPaymentMethod, currentSearchKey, onDEWModalOpen, + isDelegateAccessRestricted, + showDelegateNoAccessModal, ); }; return !isLargeScreenWidth ? ( diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 756e0357c8d47..d9028455dd6ae 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -1,10 +1,11 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useMemo, useRef} from 'react'; import type {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; // Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot // eslint-disable-next-line no-restricted-imports import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import {getButtonRole} from '@components/Button/utils'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import {useSearchContext} from '@components/Search/SearchContext'; @@ -113,6 +114,8 @@ function TransactionListItem({ ); }, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); + const handleActionButtonPress = useCallback(() => { handleActionButtonPressUtil( currentSearchHash, @@ -123,8 +126,23 @@ function TransactionListItem({ lastPaymentMethod, currentSearchKey, onDEWModalOpen, + isDelegateAccessRestricted, + showDelegateNoAccessModal, ); - }, [currentSearchHash, transactionItem, transactionPreviewData, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey, onSelectRow, item, onDEWModalOpen]); + }, [ + currentSearchHash, + transactionItem, + transactionPreviewData, + snapshotReport, + snapshotPolicy, + lastPaymentMethod, + currentSearchKey, + onSelectRow, + item, + onDEWModalOpen, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + ]); const handleCheckboxPress = useCallback(() => { onCheckboxPress?.(item); diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index c3d948364edcb..9a0a413becf1a 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -78,6 +78,8 @@ function handleActionButtonPress( lastPaymentMethod: OnyxEntry, currentSearchKey?: SearchKey, onDEWModalOpen?: () => void, + isDelegateAccessRestricted?: boolean, + onDelegateAccessRestricted?: () => void, ) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. // We need the transactionID to display the loading indicator for that list item's action. @@ -92,9 +94,17 @@ function handleActionButtonPress( switch (item.action) { case CONST.SEARCH.ACTION_TYPES.PAY: + if (isDelegateAccessRestricted) { + onDelegateAccessRestricted?.(); + return; + } getPayActionCallback(hash, item, goToItem, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey); return; case CONST.SEARCH.ACTION_TYPES.APPROVE: + if (isDelegateAccessRestricted) { + onDelegateAccessRestricted?.(); + return; + } if (hasDynamicExternalWorkflow(snapshotPolicy)) { onDEWModalOpen?.(); return; @@ -1014,17 +1024,39 @@ function isValidBulkPayOption(item: PopoverMenuItem) { /** * Handles the click event when user selects bulk pay action. */ -function handleBulkPayItemSelected( - item: PopoverMenuItem, - triggerKYCFlow: (params: ContinueActionParams) => void, - isAccountLocked: boolean, - showLockedAccountModal: () => void, - policy: OnyxEntry, - latestBankItems: BankAccountMenuItem[] | undefined, - activeAdminPolicies: Policy[], - isUserValidated: boolean | undefined, - confirmPayment?: (paymentType: PaymentMethodType | undefined, additionalData?: Record) => void, -) { +function handleBulkPayItemSelected(params: { + item: PopoverMenuItem; + triggerKYCFlow: (params: ContinueActionParams) => void; + isAccountLocked: boolean; + showLockedAccountModal: () => void; + policy: OnyxEntry; + latestBankItems: BankAccountMenuItem[] | undefined; + activeAdminPolicies: Policy[]; + isUserValidated: boolean | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: () => void; + confirmPayment?: (paymentType: PaymentMethodType | undefined, additionalData?: Record) => void; +}) { + const { + item, + triggerKYCFlow, + isAccountLocked, + showLockedAccountModal, + policy, + latestBankItems, + activeAdminPolicies, + isUserValidated, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + confirmPayment, + } = params; + + // If delegate access is restricted, we should not allow bulk pay with business bank account or bulk pay + if (isDelegateAccessRestricted && 'value' in item && (item.value === CONST.IOU.PAYMENT_TYPE.ELSEWHERE || item.value === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT)) { + showDelegateNoAccessModal(); + return; + } + const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(item.key, activeAdminPolicies, latestBankItems); // Policy id is also a last payment method so we shouldn't early return here for that case. if (!isValidBulkPayOption(item) && !selectedPolicy) { diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 2816a83330b68..739ec859c0d0c 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; @@ -96,6 +97,7 @@ function SearchPage({route}: SearchPageProps) { const styles = useThemeStyles(); const theme = useTheme(); const {isOffline} = useNetwork(); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems} = useSearchContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); @@ -227,6 +229,11 @@ function SearchPage({route}: SearchPageProps) { return; } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const activeRoute = Navigation.getActiveRoute(); const selectedOptions = selectedReports.length ? selectedReports : Object.values(selectedTransactions); @@ -318,7 +325,18 @@ function SearchPage({route}: SearchPageProps) { clearSelectedTransactions(); }); }, - [clearSelectedTransactions, hash, isOffline, lastPaymentMethods, selectedReports, selectedTransactions, policies, formatPhoneNumber], + [ + clearSelectedTransactions, + hash, + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + lastPaymentMethods, + selectedReports, + selectedTransactions, + policies, + formatPhoneNumber, + ], ); // Check if all selected transactions are from the submitter @@ -454,6 +472,11 @@ function SearchPage({route}: SearchPageProps) { return; } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + // Check if any of the selected items have DEW enabled const selectedPolicyIDList = selectedReports.length ? selectedReports.map((report) => report.policyID) @@ -544,6 +567,11 @@ function SearchPage({route}: SearchPageProps) { return; } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const isDismissed = areAllTransactionsFromSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation; if (isDismissed) { @@ -679,10 +707,11 @@ function SearchPage({route}: SearchPageProps) { styles.fontWeightNormal, styles.textWrap, expensifyIcons, + isDelegateAccessRestricted, + showDelegateNoAccessModal, dismissedHoldUseExplanation, dismissedRejectUseExplanation, areAllTransactionsFromSubmitter, - currentUserPersonalDetails?.login, ]); const handleDeleteExpenses = () => { diff --git a/src/pages/Search/SearchSelectedNarrow.tsx b/src/pages/Search/SearchSelectedNarrow.tsx index 900fe506fa2ec..e7896830b95f4 100644 --- a/src/pages/Search/SearchSelectedNarrow.tsx +++ b/src/pages/Search/SearchSelectedNarrow.tsx @@ -3,6 +3,7 @@ import React, {useContext, useRef} from 'react'; import {View} from 'react-native'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import KYCWall from '@components/KYCWall'; import {KYCWallContext} from '@components/KYCWall/KYCWallContext'; import type {PaymentMethodType} from '@components/KYCWall/types'; @@ -45,6 +46,7 @@ function SearchSelectedNarrow({options, itemsLength, currentSelectedPolicyID, cu const selectedOptionRef = useRef | null>(null); const {accountID} = useCurrentUserPersonalDetails(); const activeAdminPolicies = getActiveAdminWorkspaces(allPolicies, accountID.toString()).sort((a, b) => localeCompare(a.name || '', b.name || '')); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const handleOnMenuItemPress = (option: DropdownOption) => { if (option?.shouldCloseModalOnSelect) { @@ -74,17 +76,19 @@ function SearchSelectedNarrow({options, itemsLength, currentSelectedPolicyID, cu onPress={() => null} onOptionSelected={(item) => handleOnMenuItemPress(item)} onSubItemSelected={(subItem) => - handleBulkPayItemSelected( - subItem, + handleBulkPayItemSelected({ + item: subItem, triggerKYCFlow, isAccountLocked, showLockedAccountModal, - currentPolicy, + policy: currentPolicy, latestBankItems, activeAdminPolicies, isUserValidated, + isDelegateAccessRestricted, + showDelegateNoAccessModal, confirmPayment, - ) + }) } success isSplitButton={false}