diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index f52afd6c44cc..7825c1e24a31 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1417,6 +1417,11 @@ function MoneyReportHeader({ value: CONST.REPORT.SECONDARY_ACTIONS.REJECT, sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.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 aa54901e0c9b..ab459d298077 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -440,6 +440,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 fb72124b8cc1..3ca51ccff0f1 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -7,6 +7,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'; @@ -112,6 +113,7 @@ function SearchFiltersBar({ const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); const [searchResultsErrors] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {canBeMissing: true, selector: searchResultsErrorSelector}); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter', 'Columns']); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); // Get workspace data for the filter const {sections: workspaces, shouldShowSearchInput: shouldShowWorkspaceSearchInput} = useWorkspaceList({ @@ -850,17 +852,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 7989c8dd93f0..916309688d53 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'; @@ -73,6 +74,7 @@ function ActionCell({ const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Checkbox']); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID); const policy = usePolicy(policyID); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`, {canBeMissing: true}); @@ -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 a10faaef52c9..3f6da3d90645 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'; @@ -60,6 +61,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, @@ -70,8 +73,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 dc04da9f9ddc..fd4046bca57b 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 a076e9e157ba..ed88bd6f2dc6 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'; @@ -124,6 +125,8 @@ function TransactionListItem({ ); }, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); + const handleActionButtonPress = useCallback(() => { handleActionButtonPressUtil( currentSearchHash, @@ -134,8 +137,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/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 15849f323643..0adedfcf60dc 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -6,6 +6,7 @@ import type {GestureResponderEvent} from 'react-native'; import type {TupleToUnion} from 'type-fest'; 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 {ContinueActionParams, PaymentMethod} from '@components/KYCWall/types'; @@ -154,6 +155,7 @@ function SettlementButton({ const isInvoiceReport = (!isEmptyObject(iouReport) && isInvoiceReportUtil(iouReport)) || false; const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); + const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const kycWallRef = useContext(KYCWallContext); const shouldShowPayWithExpensifyOption = !shouldHidePaymentOptions; const shouldShowPayElsewhereOption = !shouldHidePaymentOptions && !isInvoiceReport; @@ -185,6 +187,11 @@ function SettlementButton({ } const checkForNecessaryAction = useCallback(() => { + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return true; + } + if (isAccountLocked) { showLockedAccountModal(); return true; @@ -201,7 +208,7 @@ function SettlementButton({ } return false; - }, [policy, isAccountLocked, isUserValidated, chatReportID, reportID, showLockedAccountModal]); + }, [policy, isAccountLocked, isUserValidated, chatReportID, reportID, showLockedAccountModal, isDelegateAccessRestricted, showDelegateNoAccessModal]); const getPaymentSubitems = useCallback( (payAsBusiness: boolean) => { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 919b1b191f26..94dd989fd3ba 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -81,6 +81,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. @@ -95,9 +97,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; @@ -1054,22 +1064,43 @@ 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; 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) { return; } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + if (isAccountLocked) { showLockedAccountModal(); return; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index e6ca0d6b1390..334c24261732 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'; @@ -103,6 +104,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(); @@ -267,6 +269,11 @@ function SearchPage({route}: SearchPageProps) { return; } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const activeRoute = Navigation.getActiveRoute(); const selectedOptions = selectedReports.length ? selectedReports : Object.values(selectedTransactions); @@ -359,7 +366,19 @@ function SearchPage({route}: SearchPageProps) { clearSelectedTransactions(); }); }, - [clearSelectedTransactions, hash, isOffline, lastPaymentMethods, selectedReports, selectedTransactions, policies, formatPhoneNumber, policyIDsWithVBBA], + [ + clearSelectedTransactions, + hash, + isOffline, + lastPaymentMethods, + selectedReports, + selectedTransactions, + policies, + formatPhoneNumber, + policyIDsWithVBBA, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + ], ); const [isSorting, setIsSorting] = useState(false); @@ -504,6 +523,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) @@ -552,6 +576,11 @@ function SearchPage({route}: SearchPageProps) { return; } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + if (dismissedRejectUseExplanation) { Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); } else { @@ -625,6 +654,11 @@ function SearchPage({route}: SearchPageProps) { return; } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const isDismissed = areAllTransactionsFromSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation; if (isDismissed) { @@ -815,6 +849,8 @@ function SearchPage({route}: SearchPageProps) { dismissedHoldUseExplanation, dismissedRejectUseExplanation, areAllTransactionsFromSubmitter, + isDelegateAccessRestricted, + showDelegateNoAccessModal, currentUserPersonalDetails?.accountID, ]); diff --git a/src/pages/Search/SearchSelectedNarrow.tsx b/src/pages/Search/SearchSelectedNarrow.tsx index 790d03397597..9ad0c8a16c7e 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) { @@ -75,17 +77,19 @@ function SearchSelectedNarrow({options, itemsLength, currentSelectedPolicyID, cu shouldPopoverUseScrollView={options.length >= CONST.DROPDOWN_SCROLL_THRESHOLD} 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}