diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 9db2b8c1d869..d722c48968aa 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -1,143 +1,30 @@
import {useRoute} from '@react-navigation/native';
-import {delegateEmailSelector, isUserValidatedSelector} from '@selectors/Account';
-import {hasSeenTourSelector} from '@selectors/Onboarding';
-import passthroughPolicyTagListSelector from '@selectors/PolicyTagList';
-import {validTransactionDraftsSelector} from '@selectors/TransactionDraft';
-import truncate from 'lodash/truncate';
-import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
-import {InteractionManager, View} from 'react-native';
-import type {ValueOf} from 'type-fest';
-import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies';
-import useConfirmModal from '@hooks/useConfirmModal';
-import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed';
-import {useCurrencyListActions} from '@hooks/useCurrencyList';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy';
-import useDeleteTransactions from '@hooks/useDeleteTransactions';
-import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations';
-import useExportAgainModal from '@hooks/useExportAgainModal';
-import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import React, {useEffect} from 'react';
+import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
-import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
-import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport';
-import usePaymentAnimations from '@hooks/usePaymentAnimations';
-import usePaymentOptions from '@hooks/usePaymentOptions';
-import usePermissions from '@hooks/usePermissions';
-import usePolicy from '@hooks/usePolicy';
-import useReportIsArchived from '@hooks/useReportIsArchived';
-import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
+import useReportPrimaryAction from '@hooks/useReportPrimaryAction';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
-import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
-import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
-import useStrictPolicyRules from '@hooks/useStrictPolicyRules';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import useThrottledButtonState from '@hooks/useThrottledButtonState';
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
-import useTransactionViolations from '@hooks/useTransactionViolations';
-import {duplicateReport as duplicateReportAction, duplicateExpenseTransaction as duplicateTransactionAction} from '@libs/actions/IOU/Duplicate';
-import {openOldDotLink} from '@libs/actions/Link';
-import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
-import {deleteAppReport, exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report';
-import {getExportTemplates, queueExportSearchWithTemplate, search} from '@libs/actions/Search';
-import initSplitExpense from '@libs/actions/SplitExpenses';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
-import {getExistingTransactionID} from '@libs/IOUUtils';
-import Log from '@libs/Log';
-import {getAllNonDeletedTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
-import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types';
-import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils';
-import {handleUnvalidatedAccount, selectPaymentType} from '@libs/PaymentUtils';
-import {getConnectedIntegration, getValidConnectedIntegration, isPolicyAccessible, sortPoliciesByName} from '@libs/PolicyUtils';
-import {
- getFilteredReportActionsForReportView,
- getIOUActionForTransactionID,
- getOneTransactionThreadReportID,
- getOriginalMessage,
- hasRequestFromCurrentAccount,
- isMoneyRequestAction,
-} from '@libs/ReportActionsUtils';
-import {getReportPrimaryAction} from '@libs/ReportPrimaryActionUtils';
-import {getSecondaryExportReportActions, getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils';
-import {
- canEditFieldOfMoneyRequest,
- canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
- changeMoneyRequestHoldStatus,
- generateReportID,
- getAddExpenseDropdownOptions,
- getIntegrationIcon,
- getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils,
- getNextApproverAccountID,
- getPolicyExpenseChat,
- hasHeldExpenses as hasHeldExpensesReportUtils,
- hasUpdatedTotal,
- hasViolations as hasViolationsReportUtils,
- isAllowedToApproveExpenseReport,
- isCurrentUserSubmitter,
- isDM,
- isExported as isExportedUtils,
- isInvoiceReport as isInvoiceReportUtil,
- isIOUReport as isIOUReportUtil,
- isOpenReport,
- isReportOwner,
- isSelfDM,
- navigateOnDeleteExpense,
- navigateToDetailsPage,
- shouldBlockSubmitDueToStrictPolicyRules,
-} from '@libs/ReportUtils';
-import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView';
-import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
-import {
- getChildTransactions,
- getOriginalTransactionWithSplitInfo,
- hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils,
- hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils,
- isDistanceRequest,
- isExpensifyCardTransaction,
- isPending,
- isPerDiemRequest,
- isTransactionPendingDelete,
-} from '@libs/TransactionUtils';
-import {startMoneyRequest} from '@userActions/IOU';
-import {getNavigationUrlOnMoneyRequestDelete} from '@userActions/IOU/DeleteMoneyRequest';
-import {cancelPayment, payInvoice, payMoneyRequest} from '@userActions/IOU/PayMoneyRequest';
-import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidAction, reopenReport, retractReport, submitReport, unapproveExpenseReport} from '@userActions/IOU/ReportWorkflow';
-import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report';
-import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import type * as OnyxTypes from '@src/types/onyx';
-import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
-import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
-import type {ButtonWithDropdownMenuRef, DropdownOption} from './ButtonWithDropdownMenu/types';
-import {useDelegateNoAccessActions, useDelegateNoAccessState} from './DelegateNoAccessModalProvider';
import HeaderLoadingBar from './HeaderLoadingBar';
import HeaderWithBackButton from './HeaderWithBackButton';
-import {KYCWallContext} from './KYCWall/KYCWallContext';
-import {useLockedAccountActions, useLockedAccountState} from './LockedAccountModalProvider';
-import {ModalActions} from './Modal/Global/ModalContext';
-import MoneyReportHeaderKYCDropdown from './MoneyReportHeaderKYCDropdown';
+import MoneyReportHeaderActions from './MoneyReportHeaderActions';
import MoneyReportHeaderModals from './MoneyReportHeaderModals';
-import {useMoneyReportHeaderModals} from './MoneyReportHeaderModalsContext';
import MoneyReportHeaderMoreContent from './MoneyReportHeaderMoreContent';
-import MoneyReportHeaderPrimaryAction from './MoneyReportHeaderPrimaryAction';
-import {usePersonalDetails} from './OnyxListItemProvider';
-import type {PopoverMenuItem} from './PopoverMenu';
-import BulkDuplicateHandler from './Search/BulkDuplicateHandler';
-import {useSearchActionsContext, useSearchStateContext} from './Search/SearchContext';
-import type {PaymentActionParams} from './SettlementButton/types';
-import Text from './Text';
+import {PaymentAnimationsProvider} from './PaymentAnimationsContext';
+import {useSearchActionsContext} from './Search/SearchContext';
type MoneyReportHeaderProps = {
/** The reportID of the report currently being looked at */
@@ -153,20 +40,21 @@ type MoneyReportHeaderProps = {
function MoneyReportHeader({reportID, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) {
return (
-
+
+
+
);
}
function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) {
+ const {clearSelectedTransactions} = useSearchActionsContext();
const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDProp}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
- const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
- const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -178,1627 +66,26 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt
| PlatformStackRouteProp
| PlatformStackRouteProp
>();
- const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const {login: currentUserLogin, accountID, email} = currentUserPersonalDetails;
- const personalDetails = usePersonalDetails();
- const defaultExpensePolicy = useDefaultExpensePolicy();
- const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id);
- const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
- const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`);
const {isOffline} = useNetwork();
- const allReportTransactions = useReportTransactionsCollection(reportIDProp);
- const nonDeletedTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true);
- const visibleTransactionsForThreadID = nonDeletedTransactions?.filter((t) => isOffline || t.pendingAction !== 'delete');
- const reportTransactionIDs = visibleTransactionsForThreadID?.map((t) => t.transactionID);
- const transactionThreadReportID = getOneTransactionThreadReportID(moneyRequestReport, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);
- const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`);
- const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {
- selector: isUserValidatedSelector,
- });
- const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
- const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`);
- const [session] = useOnyx(ONYXKEYS.SESSION);
- const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
- const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
- const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
-
- const activePolicy = usePolicy(activePolicyID);
- const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES);
- const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS);
- const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID);
- const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`);
- const {getCurrencyDecimals} = useCurrencyListActions();
-
- const expensifyIcons = useMemoizedLazyExpensifyIcons([
- 'Building',
- 'Buildings',
- 'Plus',
- 'Cash',
- 'Stopwatch',
- 'Send',
- 'Clear',
- 'ThumbsUp',
- 'CircularArrowBackwards',
- 'ArrowSplit',
- 'ArrowCollapse',
- 'Workflows',
- 'Trashcan',
- 'ArrowRight',
- 'ThumbsDown',
- 'Table',
- 'Info',
- 'Export',
- 'Download',
- 'XeroSquare',
- 'QBOSquare',
- 'NetSuiteSquare',
- 'IntacctSquare',
- 'QBDSquare',
- 'CertiniaSquare',
- 'GustoSquare',
- 'Feed',
- 'Location',
- 'ReceiptPlus',
- 'ExpenseCopy',
- 'Checkmark',
- 'ReportCopy',
- 'Printer',
- 'DocumentMerge',
- ]);
- const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE);
- const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${moneyRequestReport?.reportID}`);
- const [transactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
- selector: validTransactionDraftsSelector,
- });
- const draftTransactionIDs = Object.keys(transactionDrafts ?? {});
- const {translate, localeCompare} = useLocalize();
- const exportTemplates = useMemo(
- () => getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy),
- [integrationsExportTemplates, csvExportLayouts, policy, translate],
- );
- const {areStrictPolicyRulesEnabled} = useStrictPolicyRules();
- const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES);
-
- const requestParentReportAction = useMemo(() => {
- if (!reportActions || !transactionThreadReport?.parentReportActionID) {
- return null;
- }
- return reportActions.find((action): action is OnyxTypes.ReportAction => action.reportActionID === transactionThreadReport.parentReportActionID);
- }, [reportActions, transactionThreadReport?.parentReportActionID]);
+ const {translate} = useLocalize();
- const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(requestParentReportAction);
-
- const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
-
- const {transactions, nonPendingDeleteTransactions} = useMemo(() => {
- const all: OnyxTypes.Transaction[] = [];
- const filtered: OnyxTypes.Transaction[] = [];
- for (const transaction of Object.values(reportTransactions)) {
- all.push(transaction);
- if (!isTransactionPendingDelete(transaction)) {
- filtered.push(transaction);
- }
- }
- return {transactions: all, nonPendingDeleteTransactions: filtered};
- }, [reportTransactions]);
+ const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
- // When prevent self-approval is enabled & the current user is submitter AND they're submitting to themselves, we need to show the optimistic next step
- // We should always show this optimistic message for policies with preventSelfApproval
- // to avoid any flicker during transitions between online/offline states
- const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport);
- const isSubmitterSameAsNextApprover =
- isReportOwner(moneyRequestReport) && (nextApproverAccountID === moneyRequestReport?.ownerAccountID || moneyRequestReport?.managerID === moneyRequestReport?.ownerAccountID);
- const isBlockSubmitDueToPreventSelfApproval = isSubmitterSameAsNextApprover && policy?.preventSelfApproval;
- const isBlockSubmitDueToStrictPolicyRules = useMemo(() => {
- return shouldBlockSubmitDueToStrictPolicyRules(moneyRequestReport?.reportID, violations, areStrictPolicyRulesEnabled, accountID, email ?? '', transactions);
- }, [moneyRequestReport?.reportID, violations, areStrictPolicyRulesEnabled, accountID, email, transactions]);
- const shouldBlockSubmit = isBlockSubmitDueToStrictPolicyRules || isBlockSubmitDueToPreventSelfApproval;
+ const transactions = Object.values(reportTransactions);
- const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
- const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`);
-
- const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION);
- const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION);
-
- const [invoiceReceiverPolicy] = useOnyx(
- `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`,
- {},
- );
-
- const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID));
- const {deleteTransactions} = useDeleteTransactions({
- report: chatReport,
- reportActions,
- policy,
- });
- const isExported = useMemo(() => isExportedUtils(reportActions, moneyRequestReport), [reportActions, moneyRequestReport]);
- // wrapped in useMemo to improve performance because this is an operation on array
- const integrationNameFromExportMessage = useMemo(() => {
- if (!isExported) {
- return null;
- }
- return getIntegrationNameFromExportMessageUtils(reportActions);
- }, [isExported, reportActions]);
-
- const transactionViolations = useTransactionViolations(transaction?.transactionID);
- const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
- const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
- const currentTransaction = transactions.at(0);
- const [originalIOUTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(currentTransaction?.comment?.originalTransactionID)}`);
- const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`);
- const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
- const {isBetaEnabled} = usePermissions();
- const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
- const isBulkSubmitApprovePayBetaEnabled = isBetaEnabled(CONST.BETAS.BULK_SUBMIT_APPROVE_PAY);
- const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? '');
- const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations);
- const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && defaultExpensePolicy?.id !== policy?.id;
-
- const {showConfirmModal} = useConfirmModal();
- const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID);
- const {showOfflineModal, showDownloadErrorModal} = useMoneyReportHeaderModals();
-
- const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning, startAnimation, stopAnimation, startApprovedAnimation, startSubmittingAnimation} =
- usePaymentAnimations();
const styles = useThemeStyles();
- const theme = useTheme();
- const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
- const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
- const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
- const hasMultipleSplits = useMemo(() => {
- if (!transaction?.comment?.originalTransactionID) {
- return false;
- }
- const children = getChildTransactions(allTransactions, allReports, transaction.comment.originalTransactionID);
- return children.length > 1;
- }, [allTransactions, allReports, transaction?.comment?.originalTransactionID]);
- const isReportOpen = isOpenReport(moneyRequestReport);
- const shouldShowSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
-
- const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
- const [isDuplicateReportActive, temporarilyDisableDuplicateReportAction] = useThrottledButtonState();
- const dropdownMenuRef = useRef(null);
- const wasDuplicateReportTriggered = useRef(false);
-
- const handleOptionsMenuHide = useCallback(() => {
- wasDuplicateReportTriggered.current = false;
- }, []);
-
- useEffect(() => {
- if (!isDuplicateReportActive || !wasDuplicateReportTriggered.current) {
- return;
- }
- wasDuplicateReportTriggered.current = false;
- dropdownMenuRef.current?.setIsMenuVisible(false);
- }, [isDuplicateReportActive]);
-
- const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy);
- const connectedIntegration = getValidConnectedIntegration(policy);
- const connectedIntegrationFallback = getConnectedIntegration(policy);
- const hasOnlyPendingTransactions = useMemo(() => {
- return !!transactions && transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
- }, [transactions]);
- const transactionIDs = useMemo(() => transactions?.map((t) => t.transactionID) ?? [], [transactions]);
- const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS);
- const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
- const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
- selector: hasSeenTourSelector,
- });
-
- // Check if any transactions have pending RTER violations (for showing the submit confirmation modal)
- const hasAnyPendingRTERViolation = useMemo(
- () => hasAnyPendingRTERViolationTransactionUtils(transactions, allTransactionViolations, email ?? '', accountID, moneyRequestReport, policy),
- [transactions, allTransactionViolations, email, accountID, moneyRequestReport, policy],
- );
-
- const isArchivedReport = useReportIsArchived(moneyRequestReport?.reportID);
- const isChatReportArchived = useReportIsArchived(chatReport?.reportID);
-
- const canMoveSingleExpense = useMemo(() => {
- if (nonPendingDeleteTransactions.length !== 1) {
- return false;
- }
-
- const transactionToMove = nonPendingDeleteTransactions.at(0);
- if (!transactionToMove) {
- return false;
- }
-
- const iouReportAction = getIOUActionForTransactionID(reportActions, transactionToMove.transactionID);
- const canMoveExpense = canEditFieldOfMoneyRequest({
- reportAction: iouReportAction,
- fieldToEdit: CONST.EDIT_REQUEST_FIELD.REPORT,
- isChatReportArchived,
- outstandingReportsByPolicyID,
- transaction: transactionToMove,
- });
-
- const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(moneyRequestReport, isChatReportArchived);
-
- return canMoveExpense && canUserPerformWriteAction;
- }, [nonPendingDeleteTransactions, reportActions, isChatReportArchived, outstandingReportsByPolicyID, moneyRequestReport]);
-
- const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${moneyRequestReport?.reportID}`);
- const getCanIOUBePaid = useCallback(
- (onlyShowPayElsewhere = false) =>
- canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, transaction ? [transaction] : undefined, onlyShowPayElsewhere, undefined, invoiceReceiverPolicy),
- [moneyRequestReport, chatReport, policy, bankAccountList, transaction, invoiceReceiverPolicy],
- );
-
- const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport);
- const isDistanceExpenseUnsupportedForDuplicating = !!(
- isDistanceRequest(transaction) &&
- (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport))))
- );
-
- const shouldDuplicateCloseModalOnSelect =
- isDistanceExpenseUnsupportedForDuplicating ||
- isPerDiemRequestOnNonDefaultWorkspace ||
- hasCustomUnitOutOfPolicyViolation ||
- activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID;
-
- const handleDuplicateReset = useCallback(() => {
- if (shouldDuplicateCloseModalOnSelect) {
- return;
- }
- dropdownMenuRef.current?.setIsMenuVisible(false);
- }, [shouldDuplicateCloseModalOnSelect]);
-
- const [isDuplicateActive, temporarilyDisableDuplicateAction] = useThrottledButtonState(handleDuplicateReset);
-
- const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchStateContext();
- const {removeTransaction, clearSelectedTransactions} = useSearchActionsContext();
- const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true);
const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP();
const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;
-
- const {openHoldMenu, openPDFDownload, openHoldEducational, openRejectModal} = useMoneyReportHeaderModals();
-
- const showExportProgressModal = useCallback(() => {
- return showConfirmModal({
- title: translate('export.exportInProgress'),
- prompt: translate('export.conciergeWillSend'),
- confirmText: translate('common.buttonConfirm'),
- shouldShowCancelButton: false,
- });
- }, [showConfirmModal, translate]);
-
- const beginExportWithTemplate = useCallback(
- (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => {
- if (isOffline) {
- showOfflineModal();
- return;
- }
-
- if (!moneyRequestReport) {
- return;
- }
-
- showExportProgressModal().then((result) => {
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- clearSelectedTransactions(undefined, true);
- });
- queueExportSearchWithTemplate({
- templateName,
- templateType,
- jsonQuery: '{}',
- reportIDList: [moneyRequestReport.reportID],
- transactionIDList,
- policyID,
- });
- },
- [isOffline, moneyRequestReport, showExportProgressModal, clearSelectedTransactions, showOfflineModal],
- );
-
- const isOnSearch = route.name.toLowerCase().startsWith('search');
- const {
- options: originalSelectedTransactionsOptions,
- handleDeleteTransactions,
- handleDeleteTransactionsWithNavigation,
- isDuplicateOptionVisible,
- setDuplicateHandler,
- allTransactions: allTransactionsForDuplicate,
- allReports: allReportsForDuplicate,
- } = useSelectedTransactionsActions({
- report: moneyRequestReport,
- reportActions,
- allTransactionsLength: transactions.length,
- session,
- onExportFailed: showDownloadErrorModal,
- onExportOffline: showOfflineModal,
- policy,
- beginExportWithTemplate: (templateName, templateType, transactionIDList, policyID) => beginExportWithTemplate(templateName, templateType, transactionIDList, policyID),
- isOnSearch,
- });
-
- const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
- const onlyShowPayElsewhere = useMemo(() => {
- return !canIOUBePaid && getCanIOUBePaid(true);
- }, [canIOUBePaid, getCanIOUBePaid]);
-
- const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
-
- const shouldShowApproveButton = useMemo(
- () => (canApproveIOU(moneyRequestReport, policy, reportMetadata, transactions) && !hasOnlyPendingTransactions) || isApprovedAnimationRunning,
- [moneyRequestReport, policy, reportMetadata, transactions, hasOnlyPendingTransactions, isApprovedAnimationRunning],
- );
-
- const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport);
-
- const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
- const {isDelegateAccessRestricted} = useDelegateNoAccessState();
- const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
- const {isAccountLocked} = useLockedAccountState();
- const {showLockedAccountModal} = useLockedAccountActions();
- const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES);
- const kycWallRef = useContext(KYCWallContext);
- const [betas] = useOnyx(ONYXKEYS.BETAS);
const isReportInRHP = route.name !== SCREENS.REPORT;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT;
- const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport);
- const isChatReportDM = isDM(chatReport);
-
- const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID);
- const isSelectionModePaymentRef = useRef(false);
- const confirmPayment = useCallback(
- ({paymentType: type, payAsBusiness, methodID, paymentMethod}: PaymentActionParams) => {
- if (!type || !chatReport) {
- return;
- }
- const isFromSelectionMode = isSelectionModePaymentRef.current;
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- } else if (isAnyTransactionOnHold) {
- openHoldMenu({
- requestType: CONST.IOU.REPORT_ACTION_TYPE.PAY,
- paymentType: type,
- methodID,
- onConfirm: () => {
- if (isFromSelectionMode) {
- clearSelectedTransactions(true);
- return;
- }
- startAnimation();
- },
- }).then(() => {
- isSelectionModePaymentRef.current = false;
- });
- } else if (isInvoiceReport) {
- if (!isFromSelectionMode) {
- startAnimation();
- }
- payInvoice({
- paymentMethodType: type,
- chatReport,
- invoiceReport: moneyRequestReport,
- invoiceReportCurrentNextStepDeprecated: nextStep,
- introSelected,
- currentUserAccountIDParam: accountID,
- currentUserEmailParam: email ?? '',
- payAsBusiness,
- existingB2BInvoiceReport,
- methodID,
- paymentMethod,
- activePolicy,
- betas,
- isSelfTourViewed,
- });
- if (isFromSelectionMode) {
- clearSelectedTransactions(true);
- }
- } else {
- if (!isFromSelectionMode) {
- startAnimation();
- }
- payMoneyRequest({
- paymentType: type,
- chatReport,
- iouReport: moneyRequestReport,
- introSelected,
- iouReportCurrentNextStepDeprecated: nextStep,
- currentUserAccountID: accountID,
- activePolicy,
- policy,
- betas,
- isSelfTourViewed,
- userBillingGracePeriodEnds,
- amountOwed,
- ownerBillingGracePeriodEnd,
- methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined,
- onPaid: () => {
- if (isFromSelectionMode) {
- return;
- }
- startAnimation();
- },
- });
- if (currentSearchQueryJSON && !isOffline) {
- search({
- searchKey: currentSearchKey,
- shouldCalculateTotals,
- offset: 0,
- queryJSON: currentSearchQueryJSON,
- isOffline,
- isLoading: !!currentSearchResults?.search?.isLoading,
- });
- }
- if (isFromSelectionMode) {
- clearSelectedTransactions(true);
- }
- }
- },
- [
- chatReport,
- isDelegateAccessRestricted,
- isAnyTransactionOnHold,
- isInvoiceReport,
- showDelegateNoAccessModal,
- openHoldMenu,
- startAnimation,
- moneyRequestReport,
- nextStep,
- introSelected,
- accountID,
- email,
- existingB2BInvoiceReport,
- activePolicy,
- policy,
- currentSearchQueryJSON,
- isOffline,
- currentSearchKey,
- shouldCalculateTotals,
- currentSearchResults?.search?.isLoading,
- betas,
- isSelfTourViewed,
- userBillingGracePeriodEnds,
- clearSelectedTransactions,
- amountOwed,
- ownerBillingGracePeriodEnd,
- ],
- );
-
- useEffect(() => {
- if (selectedTransactionIDs.length !== 0) {
- return;
- }
- isSelectionModePaymentRef.current = false;
- }, [selectedTransactionIDs.length]);
-
- const confirmApproval = useCallback(
- (skipAnimation = false) => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- } else if (isAnyTransactionOnHold) {
- openHoldMenu({
- requestType: CONST.IOU.REPORT_ACTION_TYPE.APPROVE,
- onConfirm: () => {
- if (skipAnimation) {
- clearSelectedTransactions(true);
- return;
- }
- startApprovedAnimation();
- },
- });
- } else {
- if (!skipAnimation) {
- startApprovedAnimation();
- }
- approveMoneyRequest({
- expenseReport: moneyRequestReport,
- expenseReportPolicy: policy,
- policy,
- currentUserAccountIDParam: accountID,
- currentUserEmailParam: email ?? '',
- hasViolations,
- isASAPSubmitBetaEnabled,
- expenseReportCurrentNextStepDeprecated: nextStep,
- betas,
- userBillingGracePeriodEnds,
- amountOwed,
- ownerBillingGracePeriodEnd,
- full: true,
- onApproved: () => {
- if (skipAnimation) {
- return;
- }
- startApprovedAnimation();
- },
- delegateEmail,
- });
- if (skipAnimation) {
- clearSelectedTransactions(true);
- }
- }
- },
- [
- policy,
- isDelegateAccessRestricted,
- showDelegateNoAccessModal,
- isAnyTransactionOnHold,
- openHoldMenu,
- startApprovedAnimation,
- moneyRequestReport,
- accountID,
- email,
- hasViolations,
- isASAPSubmitBetaEnabled,
- nextStep,
- betas,
- userBillingGracePeriodEnds,
- amountOwed,
- clearSelectedTransactions,
- ownerBillingGracePeriodEnd,
- delegateEmail,
- ],
- );
-
- const handleMarkPendingRTERTransactionsAsCash = useCallback(() => {
- markPendingRTERTransactionsAsCash(transactions, allTransactionViolations, reportActions);
- }, [transactions, allTransactionViolations, reportActions]);
-
- const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash);
-
- const handleSubmitReport = useCallback(
- (skipAnimation = false) => {
- if (!moneyRequestReport || shouldBlockSubmit) {
- return;
- }
-
- const doSubmit = () => {
- submitReport({
- expenseReport: moneyRequestReport,
- policy,
- currentUserAccountIDParam: accountID,
- currentUserEmailParam: email ?? '',
- hasViolations,
- isASAPSubmitBetaEnabled,
- expenseReportCurrentNextStepDeprecated: nextStep,
- userBillingGracePeriodEnds,
- amountOwed,
- onSubmitted: () => {
- if (skipAnimation) {
- return;
- }
- startSubmittingAnimation();
- },
- ownerBillingGracePeriodEnd,
- delegateEmail,
- });
- if (currentSearchQueryJSON && !isOffline) {
- search({
- searchKey: currentSearchKey,
- shouldCalculateTotals,
- offset: 0,
- queryJSON: currentSearchQueryJSON,
- isOffline,
- isLoading: !!currentSearchResults?.search?.isLoading,
- });
- }
- if (skipAnimation) {
- clearSelectedTransactions(true);
- }
- };
- confirmPendingRTERAndProceed(doSubmit);
- },
- [
- moneyRequestReport,
- shouldBlockSubmit,
- policy,
- startSubmittingAnimation,
- accountID,
- email,
- hasViolations,
- isASAPSubmitBetaEnabled,
- nextStep,
- userBillingGracePeriodEnds,
- amountOwed,
- currentSearchQueryJSON,
- isOffline,
- currentSearchKey,
- shouldCalculateTotals,
- currentSearchResults?.search?.isLoading,
- clearSelectedTransactions,
- confirmPendingRTERAndProceed,
- ownerBillingGracePeriodEnd,
- delegateEmail,
- ],
- );
-
- const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector});
- const targetPolicyTags = useMemo(
- () => (defaultExpensePolicy ? (allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${defaultExpensePolicy.id}`] ?? {}) : {}),
- [defaultExpensePolicy, allPolicyTags],
- );
-
- const duplicateExpenseTransaction = useCallback(
- (transactionList: OnyxTypes.Transaction[]) => {
- if (!transactionList.length) {
- return;
- }
-
- const optimisticChatReportID = generateReportID();
- const optimisticIOUReportID = generateReportID();
- const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${defaultExpensePolicy?.id}`] ?? {};
-
- for (const item of transactionList) {
- const existingTransactionID = getExistingTransactionID(item.linkedTrackedExpenseReportAction);
- const existingTransactionDraft = existingTransactionID ? transactionDrafts?.[existingTransactionID] : undefined;
-
- duplicateTransactionAction({
- transaction: item,
- optimisticChatReportID,
- optimisticIOUReportID,
- isASAPSubmitBetaEnabled,
- introSelected,
- activePolicyID,
- quickAction,
- policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
- isSelfTourViewed,
- customUnitPolicyID: policy?.id,
- targetPolicy: defaultExpensePolicy ?? undefined,
- targetPolicyCategories: activePolicyCategories,
- targetReport: activePolicyExpenseChat,
- existingTransactionDraft,
- draftTransactionIDs,
- betas,
- personalDetails,
- recentWaypoints,
- targetPolicyTags,
- });
- }
- },
- [
- activePolicyExpenseChat,
- activePolicyID,
- allPolicyCategories,
- transactionDrafts,
- defaultExpensePolicy,
- draftTransactionIDs,
- introSelected,
- isASAPSubmitBetaEnabled,
- quickAction,
- policyRecentlyUsedCurrencies,
- policy?.id,
- isSelfTourViewed,
- betas,
- personalDetails,
- recentWaypoints,
- targetPolicyTags,
- ],
- );
-
- const primaryAction = useMemo(() => {
- return getReportPrimaryAction({
- currentUserLogin: currentUserLogin ?? '',
- currentUserAccountID: accountID,
- report: moneyRequestReport,
- chatReport,
- reportTransactions: nonPendingDeleteTransactions,
- violations,
- bankAccountList,
- policy,
- reportNameValuePairs,
- reportActions,
- reportMetadata,
- isChatReportArchived,
- invoiceReceiverPolicy,
- isPaidAnimationRunning,
- isApprovedAnimationRunning,
- isSubmittingAnimationRunning,
- });
- }, [
- isPaidAnimationRunning,
- isApprovedAnimationRunning,
- isSubmittingAnimationRunning,
- moneyRequestReport,
- chatReport,
- nonPendingDeleteTransactions,
- violations,
- policy,
- reportNameValuePairs,
- reportActions,
- reportMetadata,
- isChatReportArchived,
- invoiceReceiverPolicy,
- currentUserLogin,
- accountID,
- bankAccountList,
- ]);
-
- const getAmount = (actionType: ValueOf) => ({
- formattedAmount: getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, actionType, nonPendingDeleteTransactions),
- });
-
- const {formattedAmount: totalAmount} = getAmount(CONST.REPORT.PRIMARY_ACTIONS.PAY);
-
- const paymentButtonOptions = usePaymentOptions({
- currency: moneyRequestReport?.currency,
- iouReport: moneyRequestReport,
- chatReportID: chatReport?.reportID,
- formattedAmount: totalAmount,
- policyID: moneyRequestReport?.policyID,
- onPress: confirmPayment,
- shouldHidePaymentOptions: !shouldShowPayButton,
- shouldShowApproveButton,
- shouldDisableApproveButton,
- onlyShowPayElsewhere,
- });
-
- const activeAdminPolicies = useActiveAdminPolicies();
-
- const workspacePolicyOptions = useMemo(() => {
- if (!isIOUReportUtil(moneyRequestReport)) {
- return [];
- }
-
- const hasPersonalPaymentOption = paymentButtonOptions.some((opt) => opt.value === CONST.IOU.PAYMENT_TYPE.EXPENSIFY);
- if (!hasPersonalPaymentOption || !activeAdminPolicies.length) {
- return [];
- }
-
- const canUseBusinessBankAccount = moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport, accountID ?? CONST.DEFAULT_NUMBER_ID);
- if (!canUseBusinessBankAccount) {
- return [];
- }
-
- return sortPoliciesByName(activeAdminPolicies, localeCompare);
- }, [moneyRequestReport, paymentButtonOptions, activeAdminPolicies, accountID, localeCompare]);
-
- const buildPaymentSubMenuItems = useCallback(
- (onWorkspaceSelected: (workspacePolicy: OnyxTypes.Policy) => void): PopoverMenuItem[] => {
- if (!workspacePolicyOptions.length) {
- return Object.values(paymentButtonOptions);
- }
-
- const result: PopoverMenuItem[] = [];
- for (const opt of Object.values(paymentButtonOptions)) {
- result.push(opt);
- if (opt.value === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
- for (const wp of workspacePolicyOptions) {
- result.push({
- text: translate('iou.payWithPolicy', truncate(wp.name, {length: CONST.ADDITIONAL_ALLOWED_CHARACTERS}), ''),
- icon: expensifyIcons.Building,
- onSelected: () => onWorkspaceSelected(wp),
- });
- }
- }
- }
-
- return result;
- },
- [workspacePolicyOptions, paymentButtonOptions, translate, expensifyIcons.Building],
- );
-
- const addExpenseDropdownOptions = useMemo(
- () =>
- getAddExpenseDropdownOptions({
- translate,
- icons: expensifyIcons,
- iouReportID: moneyRequestReport?.reportID,
- policy,
- userBillingGracePeriodEnds,
- draftTransactionIDs,
- amountOwed,
- ownerBillingGracePeriodEnd,
- lastDistanceExpenseType,
- }),
- [moneyRequestReport?.reportID, policy, userBillingGracePeriodEnds, amountOwed, lastDistanceExpenseType, expensifyIcons, translate, ownerBillingGracePeriodEnd, draftTransactionIDs],
- );
-
- const exportSubmenuOptions: Record> = useMemo(() => {
- const options: Record> = {
- [CONST.REPORT.EXPORT_OPTIONS.DOWNLOAD_CSV]: {
- text: translate('export.basicExport'),
- icon: expensifyIcons.Table,
- value: CONST.REPORT.EXPORT_OPTIONS.DOWNLOAD_CSV,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
- onSelected: () => {
- if (!moneyRequestReport) {
- return;
- }
- if (isOffline) {
- showOfflineModal();
- return;
- }
- exportReportToCSV(
- {
- reportID: moneyRequestReport.reportID,
- transactionIDList: transactionIDs,
- },
- showDownloadErrorModal,
- translate,
- );
- },
- },
- [CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION]: {
- text: translate('workspace.common.exportIntegrationSelected', {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectionName: connectedIntegrationFallback!,
- }),
- icon: (() => {
- return getIntegrationIcon(connectedIntegration ?? connectedIntegrationFallback, expensifyIcons);
- })(),
- displayInDefaultIconColor: true,
- additionalIconStyles: styles.integrationIcon,
- value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
- onSelected: () => {
- if (!connectedIntegration || !moneyRequestReport) {
- return;
- }
- if (isExported) {
- triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION);
- return;
- }
- exportToIntegration(moneyRequestReport?.reportID, connectedIntegration);
- },
- },
- [CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED]: {
- text: translate('workspace.common.markAsExported'),
- icon: (() => {
- return getIntegrationIcon(connectedIntegration ?? connectedIntegrationFallback, expensifyIcons);
- })(),
- additionalIconStyles: styles.integrationIcon,
- displayInDefaultIconColor: true,
- value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
- onSelected: () => {
- if (!connectedIntegration || !moneyRequestReport) {
- return;
- }
- if (isExported) {
- triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED);
- return;
- }
- markAsManuallyExported([moneyRequestReport?.reportID ?? CONST.DEFAULT_NUMBER_ID], connectedIntegration);
- },
- },
- };
-
- for (const template of exportTemplates) {
- options[template.name] = {
- text: template.name,
- icon: expensifyIcons.Table,
- value: template.templateName,
- description: template.description,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
- onSelected: () => beginExportWithTemplate(template.templateName, template.type, transactionIDs, template.policyID),
- };
- }
-
- return options;
- }, [
- translate,
- expensifyIcons,
- connectedIntegrationFallback,
- styles.integrationIcon,
- moneyRequestReport,
- isOffline,
- transactionIDs,
- connectedIntegration,
- isExported,
- exportTemplates,
- beginExportWithTemplate,
- triggerExportOrConfirm,
- showOfflineModal,
- showDownloadErrorModal,
- ]);
-
- const primaryActionComponent = (
- triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION)}
- />
- );
-
- const secondaryActions = useMemo(() => {
- if (!moneyRequestReport) {
- return [];
- }
- return getSecondaryReportActions({
- currentUserLogin: currentUserLogin ?? '',
- currentUserAccountID: accountID,
- report: moneyRequestReport,
- chatReport,
- reportTransactions: nonPendingDeleteTransactions,
- originalTransaction: originalIOUTransaction,
- violations,
- bankAccountList,
- policy,
- reportNameValuePairs,
- reportActions,
- reportMetadata,
- policies,
- outstandingReportsByPolicyID,
- isChatReportArchived,
- });
- }, [
- moneyRequestReport,
- currentUserLogin,
- accountID,
- chatReport,
- nonPendingDeleteTransactions,
- originalIOUTransaction,
- violations,
- policy,
- reportNameValuePairs,
- reportActions,
- reportMetadata,
- policies,
- isChatReportArchived,
- bankAccountList,
- outstandingReportsByPolicyID,
- ]);
-
- const secondaryExportActions = useMemo(() => {
- if (!moneyRequestReport) {
- return [];
- }
- return getSecondaryExportReportActions(accountID, email ?? '', moneyRequestReport, bankAccountList, policy, exportTemplates);
- }, [moneyRequestReport, accountID, email, policy, exportTemplates, bankAccountList]);
-
- const hasSubmitAction = primaryAction === CONST.REPORT.PRIMARY_ACTIONS.SUBMIT || secondaryActions.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT);
- const hasApproveAction = primaryAction === CONST.REPORT.PRIMARY_ACTIONS.APPROVE || secondaryActions.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE);
- const hasPayAction = primaryAction === CONST.REPORT.PRIMARY_ACTIONS.PAY || secondaryActions.includes(CONST.REPORT.SECONDARY_ACTIONS.PAY);
-
- const checkForNecessaryAction = useCallback(
- (paymentMethodType?: PaymentMethodType) => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return true;
- }
- if (isAccountLocked) {
- showLockedAccountModal();
- return true;
- }
- if (!isUserValidated && paymentMethodType !== CONST.IOU.PAYMENT_TYPE.ELSEWHERE) {
- handleUnvalidatedAccount(moneyRequestReport);
- return true;
- }
-
- return false;
- },
- [isDelegateAccessRestricted, showDelegateNoAccessModal, isAccountLocked, showLockedAccountModal, isUserValidated, moneyRequestReport],
- );
-
- const selectionModeReportLevelActions = useMemo(() => {
- if (!isBulkSubmitApprovePayBetaEnabled) {
- return [];
- }
- const actions: Array & Pick> = [];
- if (hasSubmitAction && !shouldBlockSubmit) {
- actions.push({
- text: translate('common.submit'),
- icon: expensifyIcons.Send,
- value: CONST.REPORT.PRIMARY_ACTIONS.SUBMIT,
- onSelected: () => handleSubmitReport(true),
- });
- }
- if (hasApproveAction && !isBlockSubmitDueToPreventSelfApproval) {
- actions.push({
- text: translate('iou.approve'),
- icon: expensifyIcons.ThumbsUp,
- value: CONST.REPORT.PRIMARY_ACTIONS.APPROVE,
- onSelected: () => {
- isSelectionModePaymentRef.current = true;
- confirmApproval(true);
- },
- });
- }
- if (hasPayAction && !(isOffline && !canAllowSettlement)) {
- actions.push({
- text: translate('iou.settlePayment', totalAmount),
- icon: expensifyIcons.Cash,
- value: CONST.REPORT.PRIMARY_ACTIONS.PAY,
- rightIcon: expensifyIcons.ArrowRight,
- backButtonText: translate('iou.settlePayment', totalAmount),
- subMenuItems: buildPaymentSubMenuItems((wp) => {
- isSelectionModePaymentRef.current = true;
- if (checkForNecessaryAction()) {
- return;
- }
- kycWallRef.current?.continueAction?.({policy: wp});
- }),
- onSelected: () => {
- isSelectionModePaymentRef.current = true;
- },
- });
- }
- return actions;
- }, [
- isBulkSubmitApprovePayBetaEnabled,
- hasSubmitAction,
- shouldBlockSubmit,
- hasApproveAction,
- isBlockSubmitDueToPreventSelfApproval,
- hasPayAction,
- isOffline,
- canAllowSettlement,
- translate,
- handleSubmitReport,
- confirmApproval,
- totalAmount,
- buildPaymentSubMenuItems,
- checkForNecessaryAction,
- expensifyIcons.ArrowRight,
- expensifyIcons.Cash,
- expensifyIcons.Send,
- expensifyIcons.ThumbsUp,
- kycWallRef,
- ]);
-
- const connectedIntegrationName = connectedIntegration
- ? translate('workspace.accounting.connectionName', {
- connectionName: connectedIntegration,
- })
- : '';
- const unapproveWarningText = useMemo(
- () => (
-
- {translate('iou.headsUp')} {translate('iou.unapproveWithIntegrationWarning', connectedIntegrationName)}
-
- ),
- [connectedIntegrationName, styles.noWrap, styles.textStrong, translate],
- );
-
- const reopenExportedReportWarningText = (
-
- {translate('iou.headsUp')}
-
- {translate('iou.reopenExportedReportConfirmation', {
- connectionName: integrationNameFromExportMessage ?? '',
- })}
-
-
- );
-
- const secondaryActionsImplementation: Record<
- ValueOf,
- DropdownOption> & Pick
- > = {
- [CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: {
- value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS,
- text: translate('iou.viewDetails'),
- icon: expensifyIcons.Info,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.VIEW_DETAILS,
- onSelected: () => {
- navigateToDetailsPage(moneyRequestReport, Navigation.getReportRHPActiveRoute());
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.EXPORT]: {
- value: CONST.REPORT.SECONDARY_ACTIONS.EXPORT,
- text: translate('common.export'),
- backButtonText: translate('common.export'),
- icon: expensifyIcons.Export,
- rightIcon: expensifyIcons.ArrowRight,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT,
- subMenuItems: secondaryExportActions.map((action) => exportSubmenuOptions[action as string]),
- },
- [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: {
- value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF,
- text: translate('common.downloadAsPDF'),
- icon: expensifyIcons.Download,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DOWNLOAD_PDF,
- onSelected: () => {
- if (!moneyRequestReport?.reportID) {
- return;
- }
- openPDFDownload();
- exportReportToPDF({reportID: moneyRequestReport.reportID});
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.PRINT]: {
- value: CONST.REPORT.SECONDARY_ACTIONS.PRINT,
- text: translate('common.print'),
- icon: expensifyIcons.Printer,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.PRINT,
- onSelected: () => {
- if (!moneyRequestReport) {
- return;
- }
- openOldDotLink(CONST.OLDDOT_URLS.PRINTABLE_REPORT(moneyRequestReport.reportID));
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.SUBMIT]: {
- value: CONST.REPORT.SECONDARY_ACTIONS.SUBMIT,
- text: translate('common.submit'),
- icon: expensifyIcons.Send,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.SUBMIT,
- onSelected: () => {
- if (!moneyRequestReport) {
- return;
- }
-
- confirmPendingRTERAndProceed(() => {
- submitReport({
- expenseReport: moneyRequestReport,
- policy,
- currentUserAccountIDParam: accountID,
- currentUserEmailParam: email ?? '',
- hasViolations,
- isASAPSubmitBetaEnabled,
- expenseReportCurrentNextStepDeprecated: nextStep,
- userBillingGracePeriodEnds,
- amountOwed,
- ownerBillingGracePeriodEnd,
- delegateEmail,
- });
- });
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.APPROVE]: {
- text: translate('iou.approve'),
- icon: expensifyIcons.ThumbsUp,
- value: CONST.REPORT.SECONDARY_ACTIONS.APPROVE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.APPROVE,
- onSelected: confirmApproval,
- },
- [CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE]: {
- text: translate('iou.unapprove'),
- icon: expensifyIcons.CircularArrowBackwards,
- value: CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.UNAPPROVE,
- onSelected: async () => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
- if (isExported) {
- const result = await showConfirmModal({
- title: translate('iou.unapproveReport'),
- prompt: unapproveWarningText,
- confirmText: translate('iou.unapproveReport'),
- cancelText: translate('common.cancel'),
- danger: true,
- });
-
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- unapproveExpenseReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, delegateEmail);
- return;
- }
-
- unapproveExpenseReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, delegateEmail);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT]: {
- text: translate('iou.cancelPayment'),
- icon: expensifyIcons.Clear,
- value: CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.CANCEL_PAYMENT,
- onSelected: async () => {
- const result = await showConfirmModal({
- title: translate('iou.cancelPayment'),
- prompt: translate('iou.cancelPaymentConfirmation'),
- confirmText: translate('iou.cancelPayment'),
- cancelText: translate('common.dismiss'),
- danger: true,
- });
-
- if (result.action !== ModalActions.CONFIRM || !chatReport) {
- return;
- }
- cancelPayment(moneyRequestReport, chatReport, policy, isASAPSubmitBetaEnabled, accountID, email ?? '', hasViolations);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.HOLD]: {
- text: translate('iou.hold'),
- icon: expensifyIcons.Stopwatch,
- value: CONST.REPORT.SECONDARY_ACTIONS.HOLD,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.HOLD,
- onSelected: () => {
- if (!requestParentReportAction) {
- throw new Error('Parent action does not exist');
- }
-
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
- const isDismissed = isReportSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation;
-
- if (isDismissed || isChatReportDM) {
- changeMoneyRequestHoldStatus(requestParentReportAction, transaction, isOffline);
- } else if (isReportSubmitter) {
- openHoldEducational();
- } else {
- openRejectModal(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD);
- }
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD]: {
- text: translate('iou.unhold'),
- icon: expensifyIcons.Stopwatch,
- value: CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REMOVE_HOLD,
- onSelected: () => {
- if (!requestParentReportAction) {
- throw new Error('Parent action does not exist');
- }
+ // eslint-disable-next-line no-restricted-syntax -- backTo is a legacy route param, preserving existing behavior
+ const backTo = (route.params as {backTo?: Route} | undefined)?.backTo;
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
- changeMoneyRequestHoldStatus(requestParentReportAction, transaction, isOffline);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.SPLIT]: {
- text: shouldShowSplitIndicator ? translate('iou.editSplits') : translate('iou.split'),
- icon: expensifyIcons.ArrowSplit,
- value: CONST.REPORT.SECONDARY_ACTIONS.SPLIT,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.SPLIT,
- onSelected: () => {
- if (Number(transactions?.length) !== 1) {
- return;
- }
-
- initSplitExpense(currentTransaction, policy);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.MERGE]: {
- text: translate('common.merge'),
- icon: expensifyIcons.ArrowCollapse,
- value: CONST.REPORT.SECONDARY_ACTIONS.MERGE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.MERGE,
- onSelected: () => {
- if (!currentTransaction) {
- return;
- }
-
- setupMergeTransactionDataAndNavigate(currentTransaction.transactionID, [currentTransaction], localeCompare, getCurrencyDecimals);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_EXPENSE]: {
- text: isDuplicateActive ? translate('common.duplicateExpense') : translate('common.duplicated'),
- icon: isDuplicateActive ? expensifyIcons.ExpenseCopy : expensifyIcons.Checkmark,
- iconFill: isDuplicateActive ? undefined : theme.icon,
- value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_EXPENSE,
- onSelected: () => {
- if (hasCustomUnitOutOfPolicyViolation) {
- showConfirmModal({
- title: translate('common.duplicateExpense'),
- prompt: translate('iou.correctRateError'),
- confirmText: translate('common.buttonConfirm'),
- shouldShowCancelButton: false,
- });
- return;
- }
-
- if (isDistanceExpenseUnsupportedForDuplicating) {
- showConfirmModal({
- title: translate('common.duplicateExpense'),
- prompt: translate('iou.cannotDuplicateDistanceExpense'),
- confirmText: translate('common.buttonConfirm'),
- shouldShowCancelButton: false,
- });
- return;
- }
-
- if (isPerDiemRequestOnNonDefaultWorkspace) {
- showConfirmModal({
- title: translate('common.duplicateExpense'),
- prompt: translate('iou.duplicateNonDefaultWorkspacePerDiemError'),
- confirmText: translate('common.buttonConfirm'),
- shouldShowCancelButton: false,
- });
- return;
- }
-
- if (!isDuplicateActive || !transaction) {
- return;
- }
-
- temporarilyDisableDuplicateAction();
-
- duplicateExpenseTransaction([transaction]);
- },
- shouldCloseModalOnSelect: shouldDuplicateCloseModalOnSelect,
- },
- [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT]: {
- text: isDuplicateReportActive ? translate('common.duplicateReport') : translate('common.duplicated'),
- icon: isDuplicateReportActive ? expensifyIcons.ReportCopy : expensifyIcons.Checkmark,
- iconFill: isDuplicateReportActive ? undefined : theme.icon,
- value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DUPLICATE_REPORT,
- shouldShow: !!defaultExpensePolicy,
- shouldCloseModalOnSelect: false,
- onSelected: () => {
- if (!isDuplicateReportActive) {
- return;
- }
-
- temporarilyDisableDuplicateReportAction();
- wasDuplicateReportTriggered.current = true;
-
- const isSourcePolicyValid = !!policy && isPolicyAccessible(policy, currentUserLogin ?? '');
- const targetPolicyForDuplicate = isSourcePolicyValid ? policy : defaultExpensePolicy;
- const targetChatForDuplicate = isSourcePolicyValid ? chatReport : activePolicyExpenseChat;
- const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${targetPolicyForDuplicate?.id}`] ?? {};
-
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
- duplicateReportAction({
- sourceReport: moneyRequestReport,
- sourceReportTransactions: nonPendingDeleteTransactions,
- sourceReportName: moneyRequestReport?.reportName ?? '',
- targetPolicy: targetPolicyForDuplicate ?? undefined,
- targetPolicyCategories: activePolicyCategories,
- targetPolicyTags: allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyForDuplicate?.id}`] ?? {},
- parentChatReport: targetChatForDuplicate,
- ownerPersonalDetails: currentUserPersonalDetails,
- isASAPSubmitBetaEnabled,
- betas,
- personalDetails,
- quickAction,
- policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
- draftTransactionIDs,
- isSelfTourViewed,
- transactionViolations: allTransactionViolations,
- translate,
- recentWaypoints: recentWaypoints ?? [],
- });
- });
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE]: {
- text: translate('iou.changeWorkspace'),
- icon: expensifyIcons.Buildings,
- value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.CHANGE_WORKSPACE,
- shouldShow: transactions.length === 0 || nonPendingDeleteTransactions.length > 0,
- onSelected: () => {
- if (!moneyRequestReport) {
- return;
- }
- Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute()));
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE]: {
- text: translate('iou.moveExpenses'),
- icon: expensifyIcons.DocumentMerge,
- value: CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.MOVE_EXPENSE,
- shouldShow: canMoveSingleExpense,
- onSelected: () => {
- if (!moneyRequestReport || nonPendingDeleteTransactions.length !== 1) {
- return;
- }
- const transactionToMove = nonPendingDeleteTransactions.at(0);
- if (!transactionToMove?.transactionID) {
- return;
- }
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(
- CONST.IOU.ACTION.EDIT,
- CONST.IOU.TYPE.SUBMIT,
- moneyRequestReport.reportID,
- true,
- Navigation.getActiveRoute(),
- transactionToMove.transactionID,
- ),
- );
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER]: {
- text: translate('iou.changeApprover.title'),
- icon: expensifyIcons.Workflows,
- value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.CHANGE_APPROVER,
- onSelected: () => {
- if (!moneyRequestReport) {
- Log.warn('Change approver secondary action triggered without moneyRequestReport data.');
- return;
- }
- Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute()));
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.DELETE]: {
- text: translate('common.delete'),
- icon: expensifyIcons.Trashcan,
- value: CONST.REPORT.SECONDARY_ACTIONS.DELETE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DELETE,
- onSelected: async () => {
- const transactionCount = Object.keys(transactions).length;
-
- if (transactionCount === 1) {
- const result = await showConfirmModal({
- title: translate('iou.deleteExpense', {count: 1}),
- prompt: translate('iou.deleteConfirmation', {count: 1}),
- confirmText: translate('common.delete'),
- cancelText: translate('common.cancel'),
- danger: true,
- });
-
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- if (transactionThreadReportID) {
- if (!requestParentReportAction || !transaction?.transactionID) {
- throw new Error('Missing data!');
- }
- const goBackRoute = getNavigationUrlOnMoneyRequestDelete(
- transaction.transactionID,
- requestParentReportAction,
- iouReport,
- chatIOUReport,
- isChatIOUReportArchived,
- false,
- );
- const deleteNavigateBackUrl = goBackRoute ?? route.params?.backTo ?? Navigation.getActiveRoute();
- setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl);
- if (goBackRoute) {
- navigateOnDeleteExpense(goBackRoute);
- }
- // it's deleting transaction but not the report which leads to bug (that is actually also on staging)
- // Money request should be deleted when interactions are done, to not show the not found page before navigating to goBackRoute
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
- deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, isReportInSearch ? currentSearchHash : undefined, false);
- removeTransaction(transaction.transactionID);
- });
- }
- return;
- }
-
- const result = await showConfirmModal({
- title: translate('iou.deleteReport', {count: 1}),
- prompt: translate('iou.deleteReportConfirmation', {count: 1}),
- confirmText: translate('common.delete'),
- cancelText: translate('common.cancel'),
- danger: true,
- });
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- const backToRoute = route.params?.backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined);
- const deleteNavigateBackUrl = backToRoute ?? Navigation.getActiveRoute();
- setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl);
-
- Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.goBack(backToRoute);
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
- deleteAppReport({
- report: moneyRequestReport,
- selfDMReport,
- currentUserEmailParam: email ?? '',
- currentUserAccountIDParam: accountID,
- reportTransactions,
- allTransactionViolations,
- bankAccountList,
- hash: currentSearchHash,
- });
- });
- });
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.RETRACT]: {
- text: translate('iou.retract'),
- icon: expensifyIcons.CircularArrowBackwards,
- value: CONST.REPORT.SECONDARY_ACTIONS.RETRACT,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.RETRACT,
- onSelected: async () => {
- if (isExported) {
- const result = await showConfirmModal({
- title: translate('iou.reopenReport'),
- prompt: reopenExportedReportWarningText,
- confirmText: translate('iou.reopenReport'),
- cancelText: translate('common.cancel'),
- danger: true,
- });
-
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- retractReport(moneyRequestReport, chatReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, delegateEmail);
- return;
- }
- retractReport(moneyRequestReport, chatReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, delegateEmail);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.REOPEN]: {
- text: translate('iou.retract'),
- icon: expensifyIcons.CircularArrowBackwards,
- value: CONST.REPORT.SECONDARY_ACTIONS.REOPEN,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REOPEN,
- onSelected: async () => {
- if (isExported) {
- const result = await showConfirmModal({
- title: translate('iou.reopenReport'),
- prompt: reopenExportedReportWarningText,
- confirmText: translate('iou.reopenReport'),
- cancelText: translate('common.cancel'),
- danger: true,
- });
-
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- reopenReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, chatReport);
- return;
- }
- reopenReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, chatReport);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.REJECT]: {
- text: translate('common.reject'),
- icon: expensifyIcons.ThumbsDown,
- value: CONST.REPORT.SECONDARY_ACTIONS.REJECT,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REJECT,
- onSelected: () => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
- if (moneyRequestReport?.reportID) {
- Navigation.navigate(ROUTES.REJECT_EXPENSE_REPORT.getRoute(moneyRequestReport.reportID));
- }
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE]: {
- text: translate('iou.addExpense'),
- backButtonText: translate('iou.addExpense'),
- icon: expensifyIcons.Plus,
- rightIcon: expensifyIcons.ArrowRight,
- value: CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE,
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.ADD_EXPENSE,
- subMenuItems: addExpenseDropdownOptions,
- onSelected: () => {
- if (!moneyRequestReport?.reportID) {
- return;
- }
- if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) {
- Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id));
- return;
- }
- startMoneyRequest(CONST.IOU.TYPE.SUBMIT, moneyRequestReport?.reportID, draftTransactionIDs);
- },
- },
- [CONST.REPORT.SECONDARY_ACTIONS.PAY]: {
- text: translate('iou.settlePayment', totalAmount),
- icon: expensifyIcons.Cash,
- rightIcon: expensifyIcons.ArrowRight,
- value: CONST.REPORT.SECONDARY_ACTIONS.PAY,
- backButtonText: translate('iou.settlePayment', totalAmount),
- sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.PAY,
- subMenuItems: buildPaymentSubMenuItems((wp) => {
- kycWallRef.current?.continueAction?.({policy: wp});
- }),
- },
- };
- const applicableSecondaryActions = secondaryActions
- .map((action) => secondaryActionsImplementation[action])
- .filter((action) => action?.shouldShow !== false && action?.value !== primaryAction);
- useEffect(() => {
- if (!transactionThreadReportID) {
- return;
- }
- clearSelectedTransactions(true);
- // We don't need to run the effect on change of clearSelectedTransactions since it can cause the infinite loop.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [transactionThreadReportID]);
+ const primaryAction = useReportPrimaryAction(reportIDProp);
const shouldShowBackButton = shouldDisplayBackButton || shouldUseNarrowLayout;
@@ -1810,191 +97,6 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt
};
}, []);
- const showDeleteModal = useCallback(() => {
- showConfirmModal({
- title: translate('iou.deleteExpense', {
- count: selectedTransactionIDs.length,
- }),
- prompt: translate('iou.deleteConfirmation', {
- count: selectedTransactionIDs.length,
- }),
- confirmText: translate('common.delete'),
- cancelText: translate('common.cancel'),
- danger: true,
- }).then((result) => {
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- if (transactions.filter((trans) => trans.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length === selectedTransactionIDs.length) {
- const backToRoute = route.params?.backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined);
- handleDeleteTransactionsWithNavigation(backToRoute);
- } else {
- handleDeleteTransactions();
- }
- });
- }, [
- showConfirmModal,
- translate,
- selectedTransactionIDs.length,
- transactions,
- handleDeleteTransactions,
- handleDeleteTransactionsWithNavigation,
- route.params?.backTo,
- chatReport?.reportID,
- ]);
-
- const allExpensesSelected = selectedTransactionIDs.length > 0 && selectedTransactionIDs.length === nonPendingDeleteTransactions.length;
-
- const selectedTransactionsOptions = useMemo(() => {
- const mappedOptions = originalSelectedTransactionsOptions.map((option) => {
- if (option.value === CONST.REPORT.SECONDARY_ACTIONS.DELETE) {
- return {
- ...option,
- onSelected: showDeleteModal,
- };
- }
- if (option.value === CONST.REPORT.SECONDARY_ACTIONS.REJECT) {
- return {
- ...option,
- onSelected: () => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
- if (dismissedRejectUseExplanation) {
- option.onSelected?.();
- } else {
- openRejectModal(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK);
- }
- },
- };
- }
- return option;
- });
-
- if (allExpensesSelected && selectionModeReportLevelActions.length) {
- return [...selectionModeReportLevelActions, ...mappedOptions];
- }
- return mappedOptions;
- }, [
- originalSelectedTransactionsOptions,
- showDeleteModal,
- dismissedRejectUseExplanation,
- allExpensesSelected,
- selectionModeReportLevelActions,
- isDelegateAccessRestricted,
- showDelegateNoAccessModal,
- openRejectModal,
- ]);
-
- const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length && !transactionThreadReportID;
- const popoverUseScrollView = shouldPopoverUseScrollView(selectedTransactionsOptions);
-
- const hasActualPaymentOptions = paymentButtonOptions.some((opt) => Object.values(CONST.IOU.PAYMENT_TYPE).some((type) => type === opt.value));
- const hasPayInSelectionMode = allExpensesSelected && hasPayAction && hasActualPaymentOptions;
-
- const makePaymentSelectHandler = useCallback(
- (fromSelectionMode: boolean) => (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => {
- if (fromSelectionMode) {
- isSelectionModePaymentRef.current = true;
- if (checkForNecessaryAction(iouPaymentType)) {
- return;
- }
- }
- selectPaymentType({
- event,
- iouPaymentType,
- triggerKYCFlow,
- expenseReportPolicy: policy,
- policy,
- onPress: confirmPayment,
- currentAccountID: accountID,
- currentEmail: email ?? '',
- hasViolations,
- isASAPSubmitBetaEnabled,
- isUserValidated,
- confirmApproval: () => confirmApproval(),
- iouReport: moneyRequestReport,
- iouReportNextStep: nextStep,
- betas,
- userBillingGracePeriodEnds,
- amountOwed,
- ownerBillingGracePeriodEnd,
- delegateEmail,
- });
- },
- [
- checkForNecessaryAction,
- policy,
- confirmPayment,
- accountID,
- email,
- hasViolations,
- isASAPSubmitBetaEnabled,
- isUserValidated,
- confirmApproval,
- moneyRequestReport,
- nextStep,
- betas,
- userBillingGracePeriodEnds,
- amountOwed,
- ownerBillingGracePeriodEnd,
- delegateEmail,
- ],
- );
-
- const onSelectionModePaymentSelect = useMemo(() => makePaymentSelectHandler(true), [makePaymentSelectHandler]);
-
- const onPaymentSelect = useMemo(() => makePaymentSelectHandler(false), [makePaymentSelectHandler]);
-
- const selectionModeKYCSuccess = useCallback(
- (type?: PaymentMethodType) => {
- isSelectionModePaymentRef.current = true;
- confirmPayment({paymentType: type});
- },
- [confirmPayment],
- );
-
- const renderSelectionModeDropdown = useCallback(
- (wrapperStyle?: StyleProp) =>
- hasPayInSelectionMode ? (
-
- ) : (
- null}
- options={selectedTransactionsOptions}
- customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
- isSplitButton={false}
- shouldAlwaysShowDropdownMenu
- shouldPopoverUseScrollView={popoverUseScrollView}
- wrapperStyle={wrapperStyle}
- />
- ),
- [
- hasPayInSelectionMode,
- chatReport?.reportID,
- moneyRequestReport,
- onSelectionModePaymentSelect,
- selectionModeKYCSuccess,
- primaryAction,
- selectedTransactionsOptions,
- translate,
- selectedTransactionIDs.length,
- kycWallRef,
- popoverUseScrollView,
- ],
- );
-
if (isMobileSelectionModeEnabled && shouldUseNarrowLayout) {
// If mobile selection mode is enabled but only one or no transactions remain, turn it off
const visibleTransactions = transactions.filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline);
@@ -2014,79 +116,41 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt
}
return (
- <>
- {isDuplicateOptionVisible && (
- clearSelectedTransactions(true)}
+
+
+ {shouldDisplayNarrowMoreButton && (
+
+ )}
+
+ {!shouldDisplayNarrowMoreButton && (
+
)}
-
-
- {shouldDisplayNarrowMoreButton && (
-
- {!shouldShowSelectedTransactionsButton && primaryActionComponent}
- {!!applicableSecondaryActions.length && !shouldShowSelectedTransactionsButton && (
- confirmPayment({paymentType: type})}
- primaryAction={primaryAction}
- applicableSecondaryActions={applicableSecondaryActions}
- dropdownMenuRef={dropdownMenuRef}
- onOptionsMenuHide={handleOptionsMenuHide}
- ref={kycWallRef}
- />
- )}
- {shouldShowSelectedTransactionsButton && {renderSelectionModeDropdown()}}
-
- )}
-
- {!shouldDisplayNarrowMoreButton &&
- (shouldShowSelectedTransactionsButton ? (
- {renderSelectionModeDropdown(styles.w100)}
- ) : (
-
- {!!primaryAction && {primaryActionComponent}}
- {!!applicableSecondaryActions.length && (
- confirmPayment({paymentType: type})}
- primaryAction={primaryAction}
- applicableSecondaryActions={applicableSecondaryActions}
- dropdownMenuRef={dropdownMenuRef}
- onOptionsMenuHide={handleOptionsMenuHide}
- ref={kycWallRef}
- />
- )}
-
- ))}
-
-
-
-
-
- >
+
+
+
);
}
diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx
new file mode 100644
index 000000000000..f21263996f56
--- /dev/null
+++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx
@@ -0,0 +1,397 @@
+import {delegateEmailSelector, isUserValidatedSelector} from '@selectors/Account';
+import {hasSeenTourSelector} from '@selectors/Onboarding';
+import truncate from 'lodash/truncate';
+import React, {useContext, useEffect} from 'react';
+import {InteractionManager} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import type {ButtonWithDropdownMenuRef} from '@components/ButtonWithDropdownMenu/types';
+import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
+import {KYCWallContext} from '@components/KYCWall/KYCWallContext';
+import MoneyReportHeaderKYCDropdown from '@components/MoneyReportHeaderKYCDropdown';
+import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsContext';
+import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
+import {useSearchStateContext} from '@components/Search/SearchContext';
+import type {PaymentActionParams} from '@components/SettlementButton/types';
+import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useExpenseActions from '@hooks/useExpenseActions';
+import useExportActions from '@hooks/useExportActions';
+import useHoldRejectActions from '@hooks/useHoldRejectActions';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLifecycleActions from '@hooks/useLifecycleActions';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
+import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
+import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport';
+import usePaymentOptions from '@hooks/usePaymentOptions';
+import usePermissions from '@hooks/usePermissions';
+import usePolicy from '@hooks/usePolicy';
+import useReportIsArchived from '@hooks/useReportIsArchived';
+import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
+import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
+import {search} from '@libs/actions/Search';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import getPlatform from '@libs/getPlatform';
+import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils';
+import {selectPaymentType} from '@libs/PaymentUtils';
+import {sortPoliciesByName} from '@libs/PolicyUtils';
+import {getFilteredReportActionsForReportView, hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils';
+import {getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils';
+import {
+ hasHeldExpenses as hasHeldExpensesReportUtils,
+ hasViolations as hasViolationsReportUtils,
+ isAllowedToApproveExpenseReport,
+ isInvoiceReport as isInvoiceReportUtil,
+ isIOUReport as isIOUReportUtil,
+ navigateToDetailsPage,
+} from '@libs/ReportUtils';
+import {isExpensifyCardTransaction, isPending} from '@libs/TransactionUtils';
+import {payInvoice, payMoneyRequest} from '@userActions/IOU/PayMoneyRequest';
+import {canApproveIOU, canIOUBePaid as canIOUBePaidAction} from '@userActions/IOU/ReportWorkflow';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+
+type MoneyReportHeaderSecondaryActionsProps = {
+ reportID: string | undefined;
+ primaryAction: ValueOf | '';
+ isReportInSearch?: boolean;
+ backTo?: Route;
+ dropdownMenuRef?: React.RefObject;
+};
+
+function MoneyReportHeaderSecondaryActions({reportID, primaryAction, isReportInSearch, backTo, dropdownMenuRef}: MoneyReportHeaderSecondaryActionsProps) {
+ const {isPaidAnimationRunning, isApprovedAnimationRunning, startAnimation, startApprovedAnimation, startSubmittingAnimation} = usePaymentAnimationsContext();
+ const {openHoldMenu, openPDFDownload, openHoldEducational, openRejectModal} = useMoneyReportHeaderModals();
+
+ const {translate, localeCompare} = useLocalize();
+ const kycWallRef = useContext(KYCWallContext);
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`);
+ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${moneyRequestReport?.reportID}`);
+ const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${moneyRequestReport?.reportID}`);
+ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
+ const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
+ const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
+ const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
+ const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector});
+ const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+ const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
+ const [invoiceReceiverPolicy] = useOnyx(
+ `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`,
+ {},
+ );
+
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {login: currentUserLogin, accountID, email} = currentUserPersonalDetails;
+
+ const {isOffline} = useNetwork();
+ const activePolicy = usePolicy(activePolicyID);
+
+ const {isBetaEnabled} = usePermissions();
+ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
+
+ const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
+ const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
+ const allTransactions = Object.values(reportTransactions);
+ const singleTransaction = nonPendingDeleteTransactions.length === 1 ? nonPendingDeleteTransactions.at(0) : undefined;
+ const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(singleTransaction?.comment?.originalTransactionID)}`);
+
+ const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
+ const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
+
+ const isChatReportArchived = useReportIsArchived(chatReport?.reportID);
+
+ const {isDelegateAccessRestricted} = useDelegateNoAccessState();
+ const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
+ const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext();
+ const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true);
+
+ const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport);
+ const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
+ const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID);
+
+ const confirmPayment = ({paymentType: type, payAsBusiness, methodID, paymentMethod}: PaymentActionParams) => {
+ if (!type || !chatReport) {
+ return;
+ }
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ } else if (isAnyTransactionOnHold) {
+ const holdMenuParams = {
+ requestType: CONST.IOU.REPORT_ACTION_TYPE.PAY,
+ paymentType: type,
+ methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined,
+ onConfirm: () => startAnimation(),
+ };
+ if (getPlatform() === CONST.PLATFORM.IOS) {
+ // InteractionManager delays modal until current interaction completes, preventing visual glitches on iOS
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ InteractionManager.runAfterInteractions(() => openHoldMenu(holdMenuParams));
+ } else {
+ openHoldMenu(holdMenuParams);
+ }
+ } else if (isInvoiceReport) {
+ startAnimation();
+ payInvoice({
+ paymentMethodType: type,
+ chatReport,
+ invoiceReport: moneyRequestReport,
+ invoiceReportCurrentNextStepDeprecated: nextStep,
+ introSelected,
+ currentUserAccountIDParam: accountID,
+ currentUserEmailParam: email ?? '',
+ payAsBusiness,
+ existingB2BInvoiceReport,
+ methodID,
+ paymentMethod,
+ activePolicy,
+ betas,
+ isSelfTourViewed,
+ });
+ } else {
+ startAnimation();
+ payMoneyRequest({
+ paymentType: type,
+ chatReport,
+ iouReport: moneyRequestReport,
+ introSelected,
+ iouReportCurrentNextStepDeprecated: nextStep,
+ currentUserAccountID: accountID,
+ activePolicy,
+ policy,
+ betas,
+ isSelfTourViewed,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined,
+ onPaid: () => {
+ startAnimation();
+ },
+ });
+ if (currentSearchQueryJSON && !isOffline) {
+ search({
+ searchKey: currentSearchKey,
+ shouldCalculateTotals,
+ offset: 0,
+ queryJSON: currentSearchQueryJSON,
+ isOffline,
+ isLoading: !!currentSearchResults?.search?.isLoading,
+ });
+ }
+ }
+ };
+
+ // Payment button derivations
+ const canIOUBePaid = canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, undefined, false, undefined, invoiceReceiverPolicy);
+ const onlyShowPayElsewhere = !canIOUBePaid && canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, undefined, true, undefined, invoiceReceiverPolicy);
+ const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
+ const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
+ const shouldShowApproveButton = (canApproveIOU(moneyRequestReport, policy, reportMetadata, allTransactions) && !hasOnlyPendingTransactions) || isApprovedAnimationRunning;
+ const isApproveDisabled = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport);
+
+ const totalAmount = getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, CONST.REPORT.PRIMARY_ACTIONS.PAY, nonPendingDeleteTransactions);
+
+ const paymentButtonOptions = usePaymentOptions({
+ currency: moneyRequestReport?.currency,
+ iouReport: moneyRequestReport,
+ chatReportID: chatReport?.reportID,
+ formattedAmount: totalAmount,
+ policyID: moneyRequestReport?.policyID,
+ onPress: confirmPayment,
+ shouldHidePaymentOptions: !shouldShowPayButton,
+ shouldShowApproveButton,
+ shouldDisableApproveButton: isApproveDisabled,
+ onlyShowPayElsewhere,
+ });
+
+ const activeAdminPolicies = useActiveAdminPolicies();
+
+ const hasPersonalPaymentOption = paymentButtonOptions.some((opt) => opt.value === CONST.IOU.PAYMENT_TYPE.EXPENSIFY);
+ const canUseBusinessBankAccount = !!moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport, accountID ?? CONST.DEFAULT_NUMBER_ID);
+ const workspacePolicyOptions =
+ isIOUReportUtil(moneyRequestReport) && hasPersonalPaymentOption && activeAdminPolicies.length && canUseBusinessBankAccount
+ ? sortPoliciesByName(activeAdminPolicies, localeCompare)
+ : [];
+
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Info', 'Cash', 'ArrowRight', 'Building']);
+
+ const buildPaymentSubMenuItems = (onWorkspaceSelected: (workspacePolicy: OnyxTypes.Policy) => void): PopoverMenuItem[] => {
+ if (!workspacePolicyOptions.length) {
+ return Object.values(paymentButtonOptions);
+ }
+ const result: PopoverMenuItem[] = [];
+ for (const opt of Object.values(paymentButtonOptions)) {
+ result.push(opt);
+ if (opt.value === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
+ for (const wp of workspacePolicyOptions) {
+ result.push({
+ text: translate('iou.payWithPolicy', truncate(wp.name, {length: CONST.ADDITIONAL_ALLOWED_CHARACTERS}), ''),
+ icon: expensifyIcons.Building,
+ onSelected: () => onWorkspaceSelected(wp),
+ });
+ }
+ }
+ }
+ return result;
+ };
+
+ // Domain hooks
+ const lifecycleActions = useLifecycleActions({
+ reportID,
+ startApprovedAnimation,
+ startSubmittingAnimation,
+ onHoldMenuOpen: (requestType, onConfirm) => {
+ openHoldMenu({requestType, onConfirm: onConfirm ?? (() => startApprovedAnimation())});
+ },
+ });
+
+ const {
+ actions: expenseActions,
+ handleOptionsMenuHide,
+ isDuplicateReportActive,
+ wasDuplicateReportTriggeredRef,
+ } = useExpenseActions({
+ reportID,
+ isReportInSearch,
+ backTo,
+ onDuplicateReset: () => dropdownMenuRef?.current?.setIsMenuVisible(false),
+ });
+
+ useEffect(() => {
+ if (!isDuplicateReportActive || !wasDuplicateReportTriggeredRef.current) {
+ return;
+ }
+ wasDuplicateReportTriggeredRef.current = false;
+ dropdownMenuRef?.current?.setIsMenuVisible(false);
+ }, [isDuplicateReportActive, wasDuplicateReportTriggeredRef, dropdownMenuRef]);
+
+ const holdRejectActions = useHoldRejectActions({
+ reportID,
+ onHoldEducationalOpen: openHoldEducational,
+ onRejectModalOpen: openRejectModal,
+ });
+
+ const {exportActionEntries} = useExportActions({
+ reportID,
+ policy,
+ onPDFModalOpen: openPDFDownload,
+ });
+
+ // Compute list of applicable secondary action keys
+ const secondaryActions = moneyRequestReport
+ ? getSecondaryReportActions({
+ currentUserLogin: currentUserLogin ?? '',
+ currentUserAccountID: accountID,
+ report: moneyRequestReport,
+ chatReport,
+ reportTransactions: nonPendingDeleteTransactions,
+ originalTransaction,
+ violations,
+ bankAccountList,
+ policy,
+ reportNameValuePairs,
+ reportActions,
+ reportMetadata,
+ policies,
+ outstandingReportsByPolicyID,
+ isChatReportArchived,
+ })
+ : [];
+
+ // Merge all action implementations
+ const secondaryActionsImplementation: Record = {
+ [CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS,
+ text: translate('iou.viewDetails'),
+ icon: expensifyIcons.Info,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.VIEW_DETAILS,
+ onSelected: () => {
+ navigateToDetailsPage(moneyRequestReport, Navigation.getReportRHPActiveRoute());
+ },
+ },
+ ...exportActionEntries,
+ ...lifecycleActions.actions,
+ ...expenseActions,
+ ...holdRejectActions,
+ [CONST.REPORT.SECONDARY_ACTIONS.PAY]: {
+ text: translate('iou.settlePayment', totalAmount),
+ icon: expensifyIcons.Cash,
+ rightIcon: expensifyIcons.ArrowRight,
+ value: CONST.REPORT.SECONDARY_ACTIONS.PAY,
+ backButtonText: translate('iou.settlePayment', totalAmount),
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.PAY,
+ // eslint-disable-next-line react-hooks/refs -- ref is only accessed inside the callback (event handler), not during render
+ subMenuItems: buildPaymentSubMenuItems((wp) => {
+ kycWallRef.current?.continueAction?.({policy: wp});
+ }),
+ },
+ };
+
+ const applicableSecondaryActions = secondaryActions
+ .map((action) => secondaryActionsImplementation[action])
+ .filter((action) => action?.shouldShow !== false && action?.value !== primaryAction);
+
+ const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? '');
+
+ const onPaymentSelect = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => {
+ selectPaymentType({
+ event,
+ iouPaymentType,
+ triggerKYCFlow,
+ expenseReportPolicy: policy,
+ policy,
+ onPress: confirmPayment,
+ currentAccountID: accountID,
+ currentEmail: email ?? '',
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ isUserValidated,
+ confirmApproval: () => lifecycleActions.confirmApproval(),
+ iouReport: moneyRequestReport,
+ iouReportNextStep: nextStep,
+ betas,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ delegateEmail,
+ });
+ };
+
+ if (!applicableSecondaryActions.length) {
+ return null;
+ }
+
+ return (
+ confirmPayment({paymentType: type})}
+ primaryAction={primaryAction}
+ applicableSecondaryActions={applicableSecondaryActions}
+ dropdownMenuRef={dropdownMenuRef}
+ onOptionsMenuHide={handleOptionsMenuHide}
+ ref={kycWallRef}
+ />
+ );
+}
+
+export default MoneyReportHeaderSecondaryActions;
+export type {MoneyReportHeaderSecondaryActionsProps};
diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx
new file mode 100644
index 000000000000..428f6c546c98
--- /dev/null
+++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx
@@ -0,0 +1,532 @@
+import {useRoute} from '@react-navigation/native';
+import {delegateEmailSelector, isUserValidatedSelector} from '@selectors/Account';
+import {hasSeenTourSelector} from '@selectors/Onboarding';
+import truncate from 'lodash/truncate';
+import React, {useContext, useEffect, useRef} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
+import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
+import {KYCWallContext} from '@components/KYCWall/KYCWallContext';
+import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider';
+import {ModalActions} from '@components/Modal/Global/ModalContext';
+import MoneyReportHeaderKYCDropdown from '@components/MoneyReportHeaderKYCDropdown';
+import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsContext';
+import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
+import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler';
+import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import type {PaymentActionParams} from '@components/SettlementButton/types';
+import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies';
+import useConfirmModal from '@hooks/useConfirmModal';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useExportActions from '@hooks/useExportActions';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLifecycleActions from '@hooks/useLifecycleActions';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
+import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport';
+import usePaymentOptions from '@hooks/usePaymentOptions';
+import usePermissions from '@hooks/usePermissions';
+import usePolicy from '@hooks/usePolicy';
+import useReportIsArchived from '@hooks/useReportIsArchived';
+import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
+import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
+import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
+import useTransactionThreadReport from '@hooks/useTransactionThreadReport';
+import {search} from '@libs/actions/Search';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
+import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils';
+import {handleUnvalidatedAccount, selectPaymentType} from '@libs/PaymentUtils';
+import {sortPoliciesByName} from '@libs/PolicyUtils';
+import {hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils';
+import {getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils';
+import {hasHeldExpenses, hasUpdatedTotal, hasViolations as hasViolationsReportUtils, isInvoiceReport as isInvoiceReportUtil, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils';
+import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView';
+import {isTransactionPendingDelete} from '@libs/TransactionUtils';
+import {payInvoice, payMoneyRequest} from '@userActions/IOU/PayMoneyRequest';
+import {canIOUBePaid as canIOUBePaidAction} from '@userActions/IOU/ReportWorkflow';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+
+const PAYMENT_ICONS = ['Send', 'ThumbsUp', 'Cash', 'ArrowRight', 'Building'] as const;
+
+type MoneyReportHeaderSelectionDropdownProps = {
+ reportID: string | undefined;
+ primaryAction: ValueOf | '';
+ isReportInSearch?: boolean;
+ wrapperStyle?: StyleProp;
+};
+
+function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportInSearch, wrapperStyle}: MoneyReportHeaderSelectionDropdownProps) {
+ const route = useRoute();
+ const {startAnimation, startApprovedAnimation, startSubmittingAnimation} = usePaymentAnimationsContext();
+ const {openHoldMenu: openHoldMenuAsync, openRejectModal} = useMoneyReportHeaderModals();
+ const openHoldMenu = (params: Parameters[0]) => {
+ openHoldMenuAsync(params);
+ };
+ const {translate, localeCompare} = useLocalize();
+ const {isOffline} = useNetwork();
+ const {isBetaEnabled} = usePermissions();
+ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
+ const isBulkSubmitApprovePayBetaEnabled = isBetaEnabled(CONST.BETAS.BULK_SUBMIT_APPROVE_PAY);
+ const activeAdminPolicies = useActiveAdminPolicies();
+
+ const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext();
+ const {clearSelectedTransactions} = useSearchActionsContext();
+ const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true);
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
+ const [session] = useOnyx(ONYXKEYS.SESSION);
+ const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector});
+ const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
+ const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(moneyRequestReport?.reportID)}`);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
+ const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
+ const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
+ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
+ const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(moneyRequestReport?.reportID)}`);
+ const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${getNonEmptyStringOnyxID(moneyRequestReport?.reportID)}`);
+ const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION);
+ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
+ const [invoiceReceiverPolicy] = useOnyx(
+ `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`,
+ );
+
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+ const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
+ const activePolicy = usePolicy(activePolicyID);
+ const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID);
+ const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport);
+
+ const isChatReportArchived = useReportIsArchived(chatReport?.reportID);
+ const isAnyTransactionOnHold = hasHeldExpenses(moneyRequestReport?.reportID);
+
+ const {transactionThreadReportID, reportActions} = useTransactionThreadReport(reportID);
+
+ const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
+
+ const allTransactionValues = Object.values(reportTransactions);
+ const transactions = allTransactionValues;
+ const nonPendingDeleteTransactions = allTransactionValues.filter((t) => !isTransactionPendingDelete(t));
+ const singleTransaction = nonPendingDeleteTransactions.length === 1 ? nonPendingDeleteTransactions.at(0) : undefined;
+ const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(singleTransaction?.comment?.originalTransactionID)}`);
+
+ const {accountID, email, login: currentUserLogin} = useCurrentUserPersonalDetails();
+ const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? '');
+
+ const {isDelegateAccessRestricted} = useDelegateNoAccessState();
+ const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
+ const {isAccountLocked} = useLockedAccountState();
+ const {showLockedAccountModal} = useLockedAccountActions();
+ const kycWallRef = useContext(KYCWallContext);
+
+ const {showConfirmModal} = useConfirmModal();
+
+ const isSelectionModePaymentRef = useRef(false);
+
+ useEffect(() => {
+ if (selectedTransactionIDs.length !== 0) {
+ return;
+ }
+ isSelectionModePaymentRef.current = false;
+ }, [selectedTransactionIDs.length]);
+
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(PAYMENT_ICONS);
+
+ const {beginExportWithTemplate, showOfflineModal, showDownloadErrorModal} = useExportActions({
+ reportID,
+ policy,
+ });
+
+ const {confirmApproval, handleSubmitReport, shouldBlockSubmit, isBlockSubmitDueToPreventSelfApproval} = useLifecycleActions({
+ reportID,
+ startApprovedAnimation,
+ startSubmittingAnimation,
+ onHoldMenuOpen: (requestType) => openHoldMenu({requestType, onConfirm: () => clearSelectedTransactions(true)}),
+ });
+
+ const {
+ options: originalSelectedTransactionsOptions,
+ handleDeleteTransactions,
+ handleDeleteTransactionsWithNavigation,
+ isDuplicateOptionVisible,
+ setDuplicateHandler,
+ allTransactions: allTransactionsForDuplicate,
+ allReports: allReportsForDuplicate,
+ } = useSelectedTransactionsActions({
+ report: moneyRequestReport,
+ reportActions,
+ allTransactionsLength: transactions.length,
+ session,
+ onExportFailed: showDownloadErrorModal,
+ onExportOffline: showOfflineModal,
+ policy,
+ beginExportWithTemplate,
+ isOnSearch: !!isReportInSearch,
+ });
+
+ const computedSecondaryActions = moneyRequestReport
+ ? getSecondaryReportActions({
+ currentUserLogin: currentUserLogin ?? '',
+ currentUserAccountID: accountID,
+ report: moneyRequestReport,
+ chatReport,
+ reportTransactions: nonPendingDeleteTransactions,
+ originalTransaction,
+ violations,
+ bankAccountList,
+ policy,
+ reportNameValuePairs,
+ reportActions,
+ reportMetadata,
+ policies: allPolicies,
+ outstandingReportsByPolicyID,
+ isChatReportArchived,
+ })
+ : [];
+
+ const hasSubmitAction = primaryAction === CONST.REPORT.PRIMARY_ACTIONS.SUBMIT || computedSecondaryActions.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT);
+ const hasApproveAction = primaryAction === CONST.REPORT.PRIMARY_ACTIONS.APPROVE || computedSecondaryActions.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE);
+ const hasPayAction = primaryAction === CONST.REPORT.PRIMARY_ACTIONS.PAY || computedSecondaryActions.includes(CONST.REPORT.SECONDARY_ACTIONS.PAY);
+
+ const checkForNecessaryAction = (paymentMethodType?: PaymentMethodType) => {
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return true;
+ }
+ if (isAccountLocked) {
+ showLockedAccountModal();
+ return true;
+ }
+ if (!isUserValidated && paymentMethodType !== CONST.IOU.PAYMENT_TYPE.ELSEWHERE) {
+ handleUnvalidatedAccount(moneyRequestReport);
+ return true;
+ }
+ return false;
+ };
+
+ const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy);
+ const totalAmount = getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, CONST.REPORT.PRIMARY_ACTIONS.PAY, nonPendingDeleteTransactions);
+ const canIOUBePaid = canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, undefined, false, undefined, invoiceReceiverPolicy);
+ const onlyShowPayElsewhere = !canIOUBePaid && canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, undefined, true, undefined, invoiceReceiverPolicy);
+ const isPayable = hasPayAction && canIOUBePaid;
+
+ const confirmPayment = ({paymentType: type, payAsBusiness, methodID, paymentMethod}: PaymentActionParams) => {
+ if (!type || !chatReport) {
+ return;
+ }
+ isSelectionModePaymentRef.current = true;
+
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+
+ if (isAnyTransactionOnHold) {
+ openHoldMenu({
+ requestType: CONST.IOU.REPORT_ACTION_TYPE.PAY,
+ paymentType: type,
+ methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined,
+ onConfirm: () => clearSelectedTransactions(true),
+ });
+ return;
+ }
+
+ if (isInvoiceReport) {
+ payInvoice({
+ paymentMethodType: type,
+ chatReport,
+ invoiceReport: moneyRequestReport,
+ invoiceReportCurrentNextStepDeprecated: nextStep,
+ introSelected,
+ currentUserAccountIDParam: accountID,
+ currentUserEmailParam: email ?? '',
+ payAsBusiness,
+ existingB2BInvoiceReport,
+ methodID,
+ paymentMethod,
+ activePolicy,
+ betas,
+ isSelfTourViewed,
+ });
+ } else {
+ payMoneyRequest({
+ paymentType: type,
+ chatReport,
+ iouReport: moneyRequestReport,
+ introSelected,
+ iouReportCurrentNextStepDeprecated: nextStep,
+ currentUserAccountID: accountID,
+ activePolicy,
+ policy,
+ betas,
+ isSelfTourViewed,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined,
+ onPaid: () => {
+ startAnimation();
+ },
+ });
+ if (currentSearchQueryJSON && !isOffline) {
+ search({
+ searchKey: currentSearchKey,
+ shouldCalculateTotals,
+ offset: 0,
+ queryJSON: currentSearchQueryJSON,
+ isOffline,
+ isLoading: !!currentSearchResults?.search?.isLoading,
+ });
+ }
+ }
+
+ clearSelectedTransactions(true);
+ };
+
+ const paymentButtonOptions = usePaymentOptions({
+ currency: moneyRequestReport?.currency,
+ iouReport: moneyRequestReport,
+ chatReportID: chatReport?.reportID,
+ formattedAmount: totalAmount,
+ policyID: moneyRequestReport?.policyID,
+ onPress: confirmPayment,
+ shouldHidePaymentOptions: !isPayable,
+ shouldShowApproveButton: false,
+ shouldDisableApproveButton: false,
+ onlyShowPayElsewhere,
+ });
+
+ const hasPersonalPaymentOption = paymentButtonOptions.some((opt) => opt.value === CONST.IOU.PAYMENT_TYPE.EXPENSIFY);
+ const canUseBusinessBankAccount = !!moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport, accountID ?? CONST.DEFAULT_NUMBER_ID);
+ const workspacePolicyOptions =
+ isIOUReportUtil(moneyRequestReport) && hasPersonalPaymentOption && activeAdminPolicies.length && canUseBusinessBankAccount
+ ? sortPoliciesByName(activeAdminPolicies, localeCompare)
+ : [];
+
+ const buildPaymentSubMenuItems = (onWorkspaceSelected: (workspacePolicy: OnyxTypes.Policy) => void): PopoverMenuItem[] => {
+ if (!workspacePolicyOptions.length) {
+ return Object.values(paymentButtonOptions);
+ }
+ const result: PopoverMenuItem[] = [];
+ for (const opt of Object.values(paymentButtonOptions)) {
+ result.push(opt);
+ if (opt.value === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
+ for (const wp of workspacePolicyOptions) {
+ result.push({
+ text: translate('iou.payWithPolicy', truncate(wp.name, {length: CONST.ADDITIONAL_ALLOWED_CHARACTERS}), ''),
+ icon: expensifyIcons.Building,
+ onSelected: () => onWorkspaceSelected(wp),
+ });
+ }
+ }
+ }
+ return result;
+ };
+
+ const showDeleteModal = () => {
+ showConfirmModal({
+ title: translate('iou.deleteExpense', {count: selectedTransactionIDs.length}),
+ prompt: translate('iou.deleteConfirmation', {count: selectedTransactionIDs.length}),
+ confirmText: translate('common.delete'),
+ cancelText: translate('common.cancel'),
+ danger: true,
+ }).then((result) => {
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ const nonPendingCount = transactions.filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length;
+ if (nonPendingCount === selectedTransactionIDs.length) {
+ // eslint-disable-next-line no-restricted-syntax -- backTo is a legacy route param, preserving existing behavior
+ const backToRoute = ((route.params as {backTo?: Route} | undefined)?.backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined)) as
+ | Route
+ | undefined;
+ handleDeleteTransactionsWithNavigation(backToRoute);
+ } else {
+ handleDeleteTransactions();
+ }
+ });
+ };
+
+ const allExpensesSelected = selectedTransactionIDs.length > 0 && selectedTransactionIDs.length === nonPendingDeleteTransactions.length;
+
+ // Ref writes below are inside onSelected callbacks that only fire on user interaction, never during render.
+ /* eslint-disable react-hooks/refs */
+ const selectionModeReportLevelActions: Array & Pick> = !isBulkSubmitApprovePayBetaEnabled
+ ? []
+ : [
+ ...(hasSubmitAction && !shouldBlockSubmit
+ ? [
+ {
+ text: translate('common.submit'),
+ icon: expensifyIcons.Send,
+ value: CONST.REPORT.PRIMARY_ACTIONS.SUBMIT,
+ onSelected: () => handleSubmitReport(true),
+ },
+ ]
+ : []),
+ ...(hasApproveAction && !isBlockSubmitDueToPreventSelfApproval
+ ? [
+ {
+ text: translate('iou.approve'),
+ icon: expensifyIcons.ThumbsUp,
+ value: CONST.REPORT.PRIMARY_ACTIONS.APPROVE,
+ onSelected: () => {
+ isSelectionModePaymentRef.current = true;
+ confirmApproval(true);
+ },
+ },
+ ]
+ : []),
+ ...(hasPayAction && !(isOffline && !canAllowSettlement)
+ ? [
+ {
+ text: translate('iou.settlePayment', totalAmount),
+ icon: expensifyIcons.Cash,
+ value: CONST.REPORT.PRIMARY_ACTIONS.PAY as string,
+ rightIcon: expensifyIcons.ArrowRight,
+ backButtonText: translate('iou.settlePayment', totalAmount),
+ subMenuItems: buildPaymentSubMenuItems((wp) => {
+ isSelectionModePaymentRef.current = true;
+ if (checkForNecessaryAction()) {
+ return;
+ }
+ kycWallRef.current?.continueAction?.({policy: wp});
+ }),
+ onSelected: () => {
+ isSelectionModePaymentRef.current = true;
+ },
+ },
+ ]
+ : []),
+ ];
+
+ const mappedOptions = originalSelectedTransactionsOptions.map((option) => {
+ if (option.value === CONST.REPORT.SECONDARY_ACTIONS.DELETE) {
+ return {...option, onSelected: showDeleteModal};
+ }
+ if (option.value === CONST.REPORT.SECONDARY_ACTIONS.REJECT) {
+ return {
+ ...option,
+ onSelected: () => {
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+ if (dismissedRejectUseExplanation) {
+ option.onSelected?.();
+ } else {
+ openRejectModal(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK);
+ }
+ },
+ };
+ }
+ return option;
+ });
+
+ const selectedTransactionsOptions = allExpensesSelected && selectionModeReportLevelActions.length ? [...selectionModeReportLevelActions, ...mappedOptions] : mappedOptions;
+
+ const popoverUseScrollView = shouldPopoverUseScrollView(selectedTransactionsOptions);
+
+ const hasActualPaymentOptions = paymentButtonOptions.some((opt) => Object.values(CONST.IOU.PAYMENT_TYPE).some((type) => type === opt.value));
+ const hasPayInSelectionMode = allExpensesSelected && hasPayAction && hasActualPaymentOptions;
+
+ const onSelectionModePaymentSelect = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => {
+ isSelectionModePaymentRef.current = true;
+ if (checkForNecessaryAction(iouPaymentType)) {
+ return;
+ }
+ selectPaymentType({
+ event,
+ iouPaymentType,
+ triggerKYCFlow,
+ expenseReportPolicy: policy,
+ policy,
+ onPress: confirmPayment,
+ currentAccountID: accountID,
+ currentEmail: email ?? '',
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ isUserValidated,
+ confirmApproval: () => confirmApproval(),
+ iouReport: moneyRequestReport,
+ iouReportNextStep: nextStep,
+ betas,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ delegateEmail,
+ });
+ };
+
+ const selectionModeKYCSuccess = (type?: PaymentMethodType) => {
+ isSelectionModePaymentRef.current = true;
+ confirmPayment({paymentType: type});
+ };
+
+ if (!selectedTransactionsOptions.length || transactionThreadReportID) {
+ return null;
+ }
+
+ const bulkDuplicateHandler = isDuplicateOptionVisible ? (
+ clearSelectedTransactions(true)}
+ />
+ ) : null;
+
+ if (hasPayInSelectionMode) {
+ return (
+ <>
+ {bulkDuplicateHandler}
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {bulkDuplicateHandler}
+ null}
+ options={selectedTransactionsOptions}
+ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
+ isSplitButton={false}
+ shouldAlwaysShowDropdownMenu
+ shouldPopoverUseScrollView={popoverUseScrollView}
+ wrapperStyle={wrapperStyle}
+ />
+ >
+ );
+}
+
+export default MoneyReportHeaderSelectionDropdown;
+export type {MoneyReportHeaderSelectionDropdownProps};
diff --git a/src/components/MoneyReportHeaderActions/index.tsx b/src/components/MoneyReportHeaderActions/index.tsx
new file mode 100644
index 000000000000..624c8c52a45a
--- /dev/null
+++ b/src/components/MoneyReportHeaderActions/index.tsx
@@ -0,0 +1,101 @@
+import React, {useEffect, useRef} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import type {ButtonWithDropdownMenuRef} from '@components/ButtonWithDropdownMenu/types';
+import MoneyReportHeaderPrimaryAction from '@components/MoneyReportHeaderPrimaryAction';
+import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import useExportAgainModal from '@hooks/useExportAgainModal';
+import useOnyx from '@hooks/useOnyx';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useTransactionThreadReport from '@hooks/useTransactionThreadReport';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import MoneyReportHeaderSecondaryActions from './MoneyReportHeaderSecondaryActions';
+import MoneyReportHeaderSelectionDropdown from './MoneyReportHeaderSelectionDropdown';
+import type {MoneyReportHeaderActionsProps} from './types';
+
+/**
+ * Narrow the wide primaryAction union to what report-level secondary actions accept.
+ * TRANSACTION_PRIMARY_ACTIONS values (e.g. "keepThisOne") are irrelevant here.
+ */
+function narrowPrimaryAction(primaryAction: MoneyReportHeaderActionsProps['primaryAction']): ValueOf | '' {
+ if ((Object.values(CONST.REPORT.PRIMARY_ACTIONS) as string[]).includes(primaryAction)) {
+ return primaryAction as ValueOf;
+ }
+ return '';
+}
+
+function MoneyReportHeaderActions({reportID, primaryAction, isReportInSearch, backTo}: MoneyReportHeaderActionsProps) {
+ const styles = useThemeStyles();
+ const dropdownMenuRef = useRef(null) as React.RefObject;
+
+ // We need isSmallScreenWidth for the hold expense modal layout https://github.com/Expensify/App/pull/47990#issuecomment-2362382026
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
+ const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth;
+ const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP();
+ const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+
+ const {transactionThreadReportID} = useTransactionThreadReport(reportID);
+
+ const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID);
+
+ const {selectedTransactionIDs} = useSearchStateContext();
+ const {clearSelectedTransactions} = useSearchActionsContext();
+ const hasSelectedTransactions = !!selectedTransactionIDs.length;
+ const isTransactionThread = !!transactionThreadReportID;
+
+ useEffect(() => {
+ if (!transactionThreadReportID) {
+ return;
+ }
+
+ clearSelectedTransactions(true);
+ }, [transactionThreadReportID]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const narrowedPrimaryAction = narrowPrimaryAction(primaryAction);
+
+ if (hasSelectedTransactions && !isTransactionThread) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {!!primaryAction && (
+
+ triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION)}
+ />
+
+ )}
+
+
+ );
+}
+
+export default MoneyReportHeaderActions;
+export type {MoneyReportHeaderActionsProps};
diff --git a/src/components/MoneyReportHeaderActions/types.ts b/src/components/MoneyReportHeaderActions/types.ts
new file mode 100644
index 000000000000..605c0fb95af9
--- /dev/null
+++ b/src/components/MoneyReportHeaderActions/types.ts
@@ -0,0 +1,16 @@
+import type {ValueOf} from 'type-fest';
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
+import type CONST from '@src/CONST';
+import type {Route} from '@src/ROUTES';
+
+type SecondaryActionEntry = DropdownOption> & Pick;
+
+type MoneyReportHeaderActionsProps = {
+ reportID: string | undefined;
+ primaryAction: ValueOf | ValueOf | '';
+ isReportInSearch?: boolean;
+ backTo?: Route;
+};
+
+export type {SecondaryActionEntry, MoneyReportHeaderActionsProps};
diff --git a/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx
index 9b8a49c0c8a7..d4f2ea916bee 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx
+++ b/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import Button from '@components/Button';
+import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
@@ -9,10 +10,10 @@ import useConfirmApproval from './useConfirmApproval';
type ApprovePrimaryActionProps = {
reportID: string | undefined;
- startApprovedAnimation: () => void;
};
-function ApprovePrimaryAction({reportID, startApprovedAnimation}: ApprovePrimaryActionProps) {
+function ApprovePrimaryAction({reportID}: ApprovePrimaryActionProps) {
+ const {startApprovedAnimation} = usePaymentAnimationsContext();
const {translate} = useLocalize();
const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
diff --git a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx
index 2f38fd47c5ed..aaa9ac4ae37c 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx
+++ b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx
@@ -2,6 +2,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding';
import React from 'react';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsContext';
+import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import {useSearchStateContext} from '@components/Search/SearchContext';
import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton';
import type {PaymentActionParams} from '@components/SettlementButton/types';
@@ -29,14 +30,10 @@ import useTransactionThreadData from './useTransactionThreadData';
type PayPrimaryActionProps = {
reportID: string | undefined;
chatReportID: string | undefined;
- isPaidAnimationRunning: boolean;
- isApprovedAnimationRunning: boolean;
- stopAnimation: () => void;
- startAnimation: () => void;
- startApprovedAnimation: () => void;
};
-function PayPrimaryAction({reportID, chatReportID, isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation}: PayPrimaryActionProps) {
+function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) {
+ const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimationsContext();
const {isOffline} = useNetwork();
const {accountID, email} = useCurrentUserPersonalDetails();
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
diff --git a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx
index be07b7f1487e..cc104207b041 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx
+++ b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx
@@ -1,6 +1,7 @@
import {delegateEmailSelector} from '@selectors/Account';
import React from 'react';
import AnimatedSubmitButton from '@components/AnimatedSubmitButton';
+import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import {useSearchStateContext} from '@components/Search/SearchContext';
import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
@@ -24,12 +25,10 @@ import ONYXKEYS from '@src/ONYXKEYS';
type SubmitPrimaryActionProps = {
reportID: string | undefined;
- isSubmittingAnimationRunning: boolean;
- stopAnimation: () => void;
- startSubmittingAnimation: () => void;
};
-function SubmitPrimaryAction({reportID, isSubmittingAnimationRunning, stopAnimation, startSubmittingAnimation}: SubmitPrimaryActionProps) {
+function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) {
+ const {isSubmittingAnimationRunning, stopAnimation, startSubmittingAnimation} = usePaymentAnimationsContext();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {accountID, email} = useCurrentUserPersonalDetails();
diff --git a/src/components/MoneyReportHeaderPrimaryAction/index.tsx b/src/components/MoneyReportHeaderPrimaryAction/index.tsx
index 16015c6e54b5..ec8535ed564a 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/index.tsx
+++ b/src/components/MoneyReportHeaderPrimaryAction/index.tsx
@@ -10,41 +10,17 @@ import ReviewDuplicatesPrimaryAction from './ReviewDuplicatesPrimaryAction';
import SubmitPrimaryAction from './SubmitPrimaryAction';
import type {MoneyReportHeaderPrimaryActionProps} from './types';
-function MoneyReportHeaderPrimaryAction({
- reportID,
- chatReportID,
- primaryAction,
- isPaidAnimationRunning,
- isApprovedAnimationRunning,
- isSubmittingAnimationRunning,
- stopAnimation,
- startAnimation,
- startApprovedAnimation,
- startSubmittingAnimation,
- onExportModalOpen,
-}: MoneyReportHeaderPrimaryActionProps) {
+function MoneyReportHeaderPrimaryAction({reportID, chatReportID, primaryAction, onExportModalOpen}: MoneyReportHeaderPrimaryActionProps) {
if (!primaryAction) {
return null;
}
if (primaryAction === CONST.REPORT.PRIMARY_ACTIONS.SUBMIT) {
- return (
-
- );
+ return ;
}
if (primaryAction === CONST.REPORT.PRIMARY_ACTIONS.APPROVE) {
- return (
-
- );
+ return ;
}
if (primaryAction === CONST.REPORT.PRIMARY_ACTIONS.PAY) {
@@ -52,11 +28,6 @@ function MoneyReportHeaderPrimaryAction({
);
}
diff --git a/src/components/MoneyReportHeaderPrimaryAction/types.ts b/src/components/MoneyReportHeaderPrimaryAction/types.ts
index ee1ebff9b50b..5a3ea747939f 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/types.ts
+++ b/src/components/MoneyReportHeaderPrimaryAction/types.ts
@@ -5,13 +5,6 @@ type MoneyReportHeaderPrimaryActionProps = {
reportID: string | undefined;
chatReportID: string | undefined;
primaryAction: ValueOf | ValueOf | '';
- isPaidAnimationRunning: boolean;
- isApprovedAnimationRunning: boolean;
- isSubmittingAnimationRunning: boolean;
- stopAnimation: () => void;
- startAnimation: () => void;
- startApprovedAnimation: () => void;
- startSubmittingAnimation: () => void;
onExportModalOpen: () => void;
};
diff --git a/src/components/PaymentAnimationsContext.tsx b/src/components/PaymentAnimationsContext.tsx
new file mode 100644
index 000000000000..d2801a5debd0
--- /dev/null
+++ b/src/components/PaymentAnimationsContext.tsx
@@ -0,0 +1,21 @@
+import React, {createContext, useContext} from 'react';
+import usePaymentAnimations from '@hooks/usePaymentAnimations';
+
+type PaymentAnimationsContextType = ReturnType;
+
+const PaymentAnimationsContext = createContext(null);
+
+function usePaymentAnimationsContext(): PaymentAnimationsContextType {
+ const context = useContext(PaymentAnimationsContext);
+ if (!context) {
+ throw new Error('usePaymentAnimationsContext must be used within a PaymentAnimationsProvider');
+ }
+ return context;
+}
+
+function PaymentAnimationsProvider({children}: {children: React.ReactNode}) {
+ const animations = usePaymentAnimations();
+ return {children};
+}
+
+export {PaymentAnimationsProvider, usePaymentAnimationsContext};
diff --git a/src/hooks/useExpenseActions.ts b/src/hooks/useExpenseActions.ts
new file mode 100644
index 000000000000..a36f3a4f09c4
--- /dev/null
+++ b/src/hooks/useExpenseActions.ts
@@ -0,0 +1,566 @@
+import {hasSeenTourSelector} from '@selectors/Onboarding';
+import passthroughPolicyTagListSelector from '@selectors/PolicyTagList';
+import {validTransactionDraftsSelector} from '@selectors/TransactionDraft';
+import {useRef} from 'react';
+import {InteractionManager} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
+import {ModalActions} from '@components/Modal/Global/ModalContext';
+import type {SecondaryActionEntry} from '@components/MoneyReportHeaderActions/types';
+import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import {duplicateReport as duplicateReportAction, duplicateExpenseTransaction as duplicateTransactionAction} from '@libs/actions/IOU/Duplicate';
+import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction';
+import {deleteAppReport} from '@libs/actions/Report';
+import initSplitExpense from '@libs/actions/SplitExpenses';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getExistingTransactionID} from '@libs/IOUUtils';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import {isPolicyAccessible} from '@libs/PolicyUtils';
+import {getIOUActionForTransactionID, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {
+ canEditFieldOfMoneyRequest,
+ canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
+ generateReportID,
+ getAddExpenseDropdownOptions,
+ getPolicyExpenseChat,
+ isDM,
+ isOpenReport,
+ isSelfDM,
+ navigateOnDeleteExpense,
+} from '@libs/ReportUtils';
+import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
+import {
+ getChildTransactions,
+ getOriginalTransactionWithSplitInfo,
+ hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils,
+ isDistanceRequest,
+ isPerDiemRequest,
+ isTransactionPendingDelete,
+} from '@libs/TransactionUtils';
+import {startMoneyRequest} from '@userActions/IOU';
+import {getNavigationUrlOnMoneyRequestDelete} from '@userActions/IOU/DeleteMoneyRequest';
+import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import useConfirmModal from './useConfirmModal';
+import {useCurrencyListActions} from './useCurrencyList';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useDefaultExpensePolicy from './useDefaultExpensePolicy';
+import useDeleteTransactions from './useDeleteTransactions';
+import useDuplicateTransactionsAndViolations from './useDuplicateTransactionsAndViolations';
+import useGetIOUReportFromReportAction from './useGetIOUReportFromReportAction';
+import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
+import useLocalize from './useLocalize';
+import useOnyx from './useOnyx';
+import usePermissions from './usePermissions';
+import useReportIsArchived from './useReportIsArchived';
+import useTheme from './useTheme';
+import useThrottledButtonState from './useThrottledButtonState';
+import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport';
+import useTransactionThreadReport from './useTransactionThreadReport';
+import useTransactionViolations from './useTransactionViolations';
+
+type UseExpenseActionsParams = {
+ reportID: string | undefined;
+ isReportInSearch?: boolean;
+ backTo?: Route;
+ onDuplicateReset?: () => void;
+};
+
+type UseExpenseActionsReturn = {
+ actions: Partial, SecondaryActionEntry>>;
+ addExpenseDropdownOptions: Array>;
+ handleOptionsMenuHide: () => void;
+ isDuplicateReportActive: boolean;
+ wasDuplicateReportTriggeredRef: React.RefObject;
+};
+
+function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplicateReset}: UseExpenseActionsParams): UseExpenseActionsReturn {
+ const theme = useTheme();
+ const {translate, localeCompare} = useLocalize();
+ const {isBetaEnabled} = usePermissions();
+ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
+ const {getCurrencyDecimals} = useCurrencyListActions();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {login: currentUserLogin, accountID, email} = currentUserPersonalDetails;
+ const {currentSearchHash} = useSearchStateContext();
+ const {removeTransaction} = useSearchActionsContext();
+
+ // Report data
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+
+ const {transactionThreadReportID, transactionThreadReport, reportActions} = useTransactionThreadReport(reportID);
+
+ const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
+
+ const transactions: OnyxTypes.Transaction[] = [];
+ const nonPendingDeleteTransactions: OnyxTypes.Transaction[] = [];
+ for (const transaction of Object.values(reportTransactions)) {
+ transactions.push(transaction);
+ if (!isTransactionPendingDelete(transaction)) {
+ nonPendingDeleteTransactions.push(transaction);
+ }
+ }
+
+ const currentTransaction = transactions.at(0);
+ const requestParentReportAction =
+ reportActions?.find((action): action is OnyxTypes.ReportAction => action.reportActionID === transactionThreadReport?.parentReportActionID) ??
+ null;
+
+ const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`);
+ const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`);
+ const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(requestParentReportAction);
+
+ // Global collections
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
+ const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+ const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+ const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
+ const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES);
+ const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector});
+ const [transactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftsSelector});
+ const draftTransactionIDs = Object.keys(transactionDrafts ?? {});
+ const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS);
+ const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
+ const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES);
+ const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID);
+ const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`);
+ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
+ const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+
+ // Billing keys
+ const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
+ const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
+ const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
+ const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE);
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+
+ // Archive checks
+ const isArchivedReport = useReportIsArchived(moneyRequestReport?.reportID);
+ const isChatReportArchived = useReportIsArchived(chatReport?.reportID);
+
+ // Default expense policy / chat
+ const defaultExpensePolicy = useDefaultExpensePolicy();
+ const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id);
+
+ // Duplicate detection
+ const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID));
+
+ // Delete hook — pass chatReport (as in MoneyReportHeader) not moneyRequestReport
+ const {deleteTransactions} = useDeleteTransactions({
+ report: chatReport,
+ reportActions,
+ policy,
+ });
+
+ // Confirm modal
+ const {showConfirmModal} = useConfirmModal();
+
+ // Split indicator
+ const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
+ const hasMultipleSplits = !!transaction?.comment?.originalTransactionID && getChildTransactions(allTransactions, allReports, transaction.comment.originalTransactionID).length > 1;
+ const isReportOpen = isOpenReport(moneyRequestReport);
+ const hasSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
+
+ // Duplicate report throttle
+ const [isDuplicateReportActive, temporarilyDisableDuplicateReportAction] = useThrottledButtonState();
+ const wasDuplicateReportTriggeredRef = useRef(false);
+
+ const handleOptionsMenuHide = () => {
+ wasDuplicateReportTriggeredRef.current = false;
+ };
+
+ // The dropdown ref is owned by the caller (orchestrator) — we close the menu by calling into it.
+ // We expose an effect trigger instead: when isDuplicateReportActive flips back to true and the flag
+ // is set, the caller should call dropdownMenuRef.current?.setIsMenuVisible(false).
+ // To keep this self-contained we return the ref so the caller can react to it.
+
+ const singleTransaction = nonPendingDeleteTransactions.length === 1 ? nonPendingDeleteTransactions.at(0) : undefined;
+ const canMoveSingleExpense =
+ !!singleTransaction &&
+ canEditFieldOfMoneyRequest({
+ reportAction: getIOUActionForTransactionID(reportActions, singleTransaction.transactionID),
+ fieldToEdit: CONST.EDIT_REQUEST_FIELD.REPORT,
+ isChatReportArchived,
+ outstandingReportsByPolicyID,
+ transaction: singleTransaction,
+ }) &&
+ canUserPerformWriteActionReportUtils(moneyRequestReport, isChatReportArchived);
+
+ // Duplicate expense: unsupported / shouldClose flags
+ const transactionViolations = useTransactionViolations(transaction?.transactionID);
+ const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations);
+ const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && defaultExpensePolicy?.id !== policy?.id;
+ const isDistanceExpenseUnsupportedForDuplicating = !!(
+ isDistanceRequest(transaction) &&
+ (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport))))
+ );
+ const shouldDuplicateCloseModalOnSelect =
+ isDistanceExpenseUnsupportedForDuplicating ||
+ isPerDiemRequestOnNonDefaultWorkspace ||
+ hasCustomUnitOutOfPolicyViolation ||
+ activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID;
+
+ const handleDuplicateReset = () => {
+ if (shouldDuplicateCloseModalOnSelect) {
+ return;
+ }
+ onDuplicateReset?.();
+ };
+ const [isDuplicateActive, temporarilyDisableDuplicateAction] = useThrottledButtonState(handleDuplicateReset);
+
+ const targetPolicyTags = defaultExpensePolicy ? (allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${defaultExpensePolicy.id}`] ?? {}) : {};
+
+ const duplicateExpenseTransaction = (transactionList: OnyxTypes.Transaction[]) => {
+ if (!transactionList.length) {
+ return;
+ }
+ const optimisticChatReportID = generateReportID();
+ const optimisticIOUReportID = generateReportID();
+ const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${defaultExpensePolicy?.id}`] ?? {};
+
+ for (const item of transactionList) {
+ const existingTransactionID = getExistingTransactionID(item.linkedTrackedExpenseReportAction);
+ const existingTransactionDraft = existingTransactionID ? transactionDrafts?.[existingTransactionID] : undefined;
+
+ duplicateTransactionAction({
+ transaction: item,
+ optimisticChatReportID,
+ optimisticIOUReportID,
+ isASAPSubmitBetaEnabled,
+ introSelected,
+ activePolicyID,
+ quickAction,
+ policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
+ isSelfTourViewed,
+ customUnitPolicyID: policy?.id,
+ targetPolicy: defaultExpensePolicy ?? undefined,
+ targetPolicyCategories: activePolicyCategories,
+ targetReport: activePolicyExpenseChat,
+ existingTransactionDraft,
+ draftTransactionIDs,
+ betas,
+ personalDetails,
+ recentWaypoints,
+ targetPolicyTags,
+ });
+ }
+ };
+
+ const addExpenseDropdownOptions = getAddExpenseDropdownOptions({
+ translate,
+ icons: useMemoizedLazyExpensifyIcons(['Plus', 'ReceiptPlus', 'Location', 'Feed', 'ArrowRight']),
+ iouReportID: moneyRequestReport?.reportID,
+ policy,
+ userBillingGracePeriodEnds,
+ draftTransactionIDs,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ lastDistanceExpenseType,
+ });
+
+ const expensifyIcons = useMemoizedLazyExpensifyIcons([
+ 'Plus',
+ 'ArrowSplit',
+ 'ArrowCollapse',
+ 'ExpenseCopy',
+ 'ReportCopy',
+ 'Checkmark',
+ 'DocumentMerge',
+ 'Workflows',
+ 'Trashcan',
+ 'Buildings',
+ 'ReceiptPlus',
+ 'Location',
+ 'Feed',
+ 'ArrowRight',
+ ]);
+
+ const actions: Partial, SecondaryActionEntry>> = {
+ [CONST.REPORT.SECONDARY_ACTIONS.SPLIT]: {
+ text: hasSplitIndicator ? translate('iou.editSplits') : translate('iou.split'),
+ icon: expensifyIcons.ArrowSplit,
+ value: CONST.REPORT.SECONDARY_ACTIONS.SPLIT,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.SPLIT,
+ onSelected: () => {
+ if (transactions.length !== 1) {
+ return;
+ }
+ initSplitExpense(currentTransaction, policy);
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.MERGE]: {
+ text: translate('common.merge'),
+ icon: expensifyIcons.ArrowCollapse,
+ value: CONST.REPORT.SECONDARY_ACTIONS.MERGE,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.MERGE,
+ onSelected: () => {
+ if (!currentTransaction) {
+ return;
+ }
+ setupMergeTransactionDataAndNavigate(currentTransaction.transactionID, [currentTransaction], localeCompare, getCurrencyDecimals);
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_EXPENSE]: {
+ text: isDuplicateActive ? translate('common.duplicateExpense') : translate('common.duplicated'),
+ icon: isDuplicateActive ? expensifyIcons.ExpenseCopy : expensifyIcons.Checkmark,
+ iconFill: isDuplicateActive ? undefined : theme.icon,
+ value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_EXPENSE,
+ onSelected: () => {
+ if (hasCustomUnitOutOfPolicyViolation) {
+ showConfirmModal({
+ title: translate('common.duplicateExpense'),
+ prompt: translate('iou.correctRateError'),
+ confirmText: translate('common.buttonConfirm'),
+ shouldShowCancelButton: false,
+ });
+ return;
+ }
+
+ if (isDistanceExpenseUnsupportedForDuplicating) {
+ showConfirmModal({
+ title: translate('common.duplicateExpense'),
+ prompt: translate('iou.cannotDuplicateDistanceExpense'),
+ confirmText: translate('common.buttonConfirm'),
+ shouldShowCancelButton: false,
+ });
+ return;
+ }
+
+ if (isPerDiemRequestOnNonDefaultWorkspace) {
+ showConfirmModal({
+ title: translate('common.duplicateExpense'),
+ prompt: translate('iou.duplicateNonDefaultWorkspacePerDiemError'),
+ confirmText: translate('common.buttonConfirm'),
+ shouldShowCancelButton: false,
+ });
+ return;
+ }
+
+ if (!isDuplicateActive || !transaction) {
+ return;
+ }
+
+ temporarilyDisableDuplicateAction();
+ duplicateExpenseTransaction([transaction]);
+ },
+ shouldCloseModalOnSelect: shouldDuplicateCloseModalOnSelect,
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT]: {
+ text: isDuplicateReportActive ? translate('common.duplicateReport') : translate('common.duplicated'),
+ icon: isDuplicateReportActive ? expensifyIcons.ReportCopy : expensifyIcons.Checkmark,
+ iconFill: isDuplicateReportActive ? undefined : theme.icon,
+ value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DUPLICATE_REPORT,
+ shouldShow: !!defaultExpensePolicy,
+ shouldCloseModalOnSelect: false,
+ onSelected: () => {
+ if (!isDuplicateReportActive) {
+ return;
+ }
+
+ temporarilyDisableDuplicateReportAction();
+ wasDuplicateReportTriggeredRef.current = true;
+
+ const isSourcePolicyValid = !!policy && isPolicyAccessible(policy, currentUserLogin ?? '');
+ const targetPolicyForDuplicate = isSourcePolicyValid ? policy : defaultExpensePolicy;
+ const targetChatForDuplicate = isSourcePolicyValid ? chatReport : activePolicyExpenseChat;
+ const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${targetPolicyForDuplicate?.id}`] ?? {};
+
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ InteractionManager.runAfterInteractions(() => {
+ duplicateReportAction({
+ sourceReport: moneyRequestReport,
+ sourceReportTransactions: nonPendingDeleteTransactions,
+ sourceReportName: moneyRequestReport?.reportName ?? '',
+ targetPolicy: targetPolicyForDuplicate ?? undefined,
+ targetPolicyCategories: activePolicyCategories,
+ targetPolicyTags: allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyForDuplicate?.id}`] ?? {},
+ parentChatReport: targetChatForDuplicate,
+ ownerPersonalDetails: currentUserPersonalDetails,
+ isASAPSubmitBetaEnabled,
+ betas,
+ personalDetails,
+ quickAction,
+ policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
+ draftTransactionIDs,
+ isSelfTourViewed,
+ transactionViolations: allTransactionViolations,
+ translate,
+ recentWaypoints: recentWaypoints ?? [],
+ });
+ });
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE]: {
+ text: translate('iou.changeWorkspace'),
+ icon: expensifyIcons.Buildings,
+ value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.CHANGE_WORKSPACE,
+ shouldShow: transactions.length === 0 || nonPendingDeleteTransactions.length > 0,
+ onSelected: () => {
+ if (!moneyRequestReport) {
+ return;
+ }
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute()));
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE]: {
+ text: translate('iou.moveExpenses'),
+ icon: expensifyIcons.DocumentMerge,
+ value: CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.MOVE_EXPENSE,
+ shouldShow: canMoveSingleExpense,
+ onSelected: () => {
+ if (!moneyRequestReport || nonPendingDeleteTransactions.length !== 1) {
+ return;
+ }
+ const transactionToMove = nonPendingDeleteTransactions.at(0);
+ if (!transactionToMove?.transactionID) {
+ return;
+ }
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ CONST.IOU.TYPE.SUBMIT,
+ moneyRequestReport.reportID,
+ true,
+ Navigation.getActiveRoute(),
+ transactionToMove.transactionID,
+ ),
+ );
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER]: {
+ text: translate('iou.changeApprover.title'),
+ icon: expensifyIcons.Workflows,
+ value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.CHANGE_APPROVER,
+ onSelected: () => {
+ if (!moneyRequestReport) {
+ Log.warn('Change approver secondary action triggered without moneyRequestReport data.');
+ return;
+ }
+ Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute()));
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.DELETE]: {
+ text: translate('common.delete'),
+ icon: expensifyIcons.Trashcan,
+ value: CONST.REPORT.SECONDARY_ACTIONS.DELETE,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DELETE,
+ onSelected: async () => {
+ const transactionCount = Object.keys(transactions).length;
+
+ if (transactionCount === 1) {
+ const result = await showConfirmModal({
+ title: translate('iou.deleteExpense', {count: 1}),
+ prompt: translate('iou.deleteConfirmation', {count: 1}),
+ confirmText: translate('common.delete'),
+ cancelText: translate('common.cancel'),
+ danger: true,
+ });
+
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ if (transactionThreadReportID) {
+ if (!requestParentReportAction || !transaction?.transactionID) {
+ throw new Error('Missing data!');
+ }
+ const goBackRoute = getNavigationUrlOnMoneyRequestDelete(
+ transaction.transactionID,
+ requestParentReportAction,
+ iouReport,
+ chatIOUReport,
+ isChatIOUReportArchived,
+ false,
+ );
+ const deleteNavigateBackUrl = goBackRoute ?? backTo ?? Navigation.getActiveRoute();
+ setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl);
+ if (goBackRoute) {
+ navigateOnDeleteExpense(goBackRoute);
+ }
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ InteractionManager.runAfterInteractions(() => {
+ deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, isReportInSearch ? currentSearchHash : undefined, false);
+ removeTransaction(transaction.transactionID);
+ });
+ }
+ return;
+ }
+
+ const result = await showConfirmModal({
+ title: translate('iou.deleteReport', {count: 1}),
+ prompt: translate('iou.deleteReportConfirmation', {count: 1}),
+ confirmText: translate('common.delete'),
+ cancelText: translate('common.cancel'),
+ danger: true,
+ });
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ const backToRoute = backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined);
+ const deleteNavigateBackUrl = backToRoute ?? Navigation.getActiveRoute();
+ setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl);
+
+ Navigation.setNavigationActionToMicrotaskQueue(() => {
+ Navigation.goBack(backToRoute);
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ InteractionManager.runAfterInteractions(() => {
+ deleteAppReport({
+ report: moneyRequestReport,
+ selfDMReport,
+ currentUserEmailParam: email ?? '',
+ currentUserAccountIDParam: accountID,
+ reportTransactions,
+ allTransactionViolations,
+ bankAccountList,
+ hash: currentSearchHash,
+ });
+ });
+ });
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE]: {
+ text: translate('iou.addExpense'),
+ backButtonText: translate('iou.addExpense'),
+ icon: expensifyIcons.Plus,
+ rightIcon: expensifyIcons.ArrowRight,
+ value: CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.ADD_EXPENSE,
+ subMenuItems: addExpenseDropdownOptions,
+ onSelected: () => {
+ if (!moneyRequestReport?.reportID) {
+ return;
+ }
+ if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) {
+ Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id));
+ return;
+ }
+ startMoneyRequest(CONST.IOU.TYPE.SUBMIT, moneyRequestReport?.reportID, draftTransactionIDs);
+ },
+ },
+ };
+
+ return {
+ actions,
+ addExpenseDropdownOptions,
+ handleOptionsMenuHide,
+ isDuplicateReportActive,
+ wasDuplicateReportTriggeredRef,
+ };
+}
+
+export default useExpenseActions;
+export type {UseExpenseActionsParams, UseExpenseActionsReturn};
diff --git a/src/hooks/useExportActions.ts b/src/hooks/useExportActions.ts
new file mode 100644
index 000000000000..7b8003a80da9
--- /dev/null
+++ b/src/hooks/useExportActions.ts
@@ -0,0 +1,268 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
+import {ModalActions} from '@components/Modal/Global/ModalContext';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
+import {useSearchActionsContext} from '@components/Search/SearchContext';
+import {openOldDotLink} from '@libs/actions/Link';
+import {exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report';
+import {getExportTemplates, queueExportSearchWithTemplate} from '@libs/actions/Search';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getConnectedIntegration, getValidConnectedIntegration} from '@libs/PolicyUtils';
+import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils';
+import {getSecondaryExportReportActions} from '@libs/ReportSecondaryActionUtils';
+import {getIntegrationIcon, isExported as isExportedUtils} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxTypes from '@src/types/onyx';
+import useConfirmModal from './useConfirmModal';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useDecisionModal from './useDecisionModal';
+import useExportAgainModal from './useExportAgainModal';
+import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
+import useLocalize from './useLocalize';
+import useNetwork from './useNetwork';
+import useOnyx from './useOnyx';
+import usePaginatedReportActions from './usePaginatedReportActions';
+import useThemeStyles from './useThemeStyles';
+import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport';
+
+type UseExportActionsParams = {
+ reportID: string | undefined;
+ policy?: OnyxEntry;
+ onPDFModalOpen?: () => void;
+};
+
+type UseExportActionsReturn = {
+ exportActionEntries: Record> & Pick>;
+ secondaryExportActions: Array>;
+ beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => void;
+ showOfflineModal: () => void;
+ showDownloadErrorModal: () => void;
+};
+
+function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsParams): UseExportActionsReturn {
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
+ const styles = useThemeStyles();
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES);
+ const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS);
+
+ const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
+ const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
+
+ const {login: currentUserLogin, accountID} = useCurrentUserPersonalDetails();
+
+ const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
+ const transactionIDs = Object.values(reportTransactions).map((t) => t.transactionID);
+
+ const connectedIntegration = getValidConnectedIntegration(policy);
+ const connectedIntegrationFallback = getConnectedIntegration(policy);
+ const exportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy);
+ const isExported = isExportedUtils(reportActions, moneyRequestReport);
+
+ const {showConfirmModal} = useConfirmModal();
+ const {showDecisionModal} = useDecisionModal();
+ const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID);
+ const {clearSelectedTransactions} = useSearchActionsContext();
+
+ const expensifyIcons = useMemoizedLazyExpensifyIcons([
+ 'Table',
+ 'Export',
+ 'Download',
+ 'Printer',
+ 'XeroSquare',
+ 'QBOSquare',
+ 'NetSuiteSquare',
+ 'IntacctSquare',
+ 'QBDSquare',
+ 'CertiniaSquare',
+ 'GustoSquare',
+ 'ArrowRight',
+ ]);
+
+ const showOfflineModal = () => {
+ showDecisionModal({
+ title: translate('common.youAppearToBeOffline'),
+ prompt: translate('common.offlinePrompt'),
+ secondOptionText: translate('common.buttonConfirm'),
+ });
+ };
+
+ const showDownloadErrorModal = () => {
+ showDecisionModal({
+ title: translate('common.downloadFailedTitle'),
+ prompt: translate('common.downloadFailedDescription'),
+ secondOptionText: translate('common.buttonConfirm'),
+ });
+ };
+
+ const showExportProgressModal = () => {
+ return showConfirmModal({
+ title: translate('export.exportInProgress'),
+ prompt: translate('export.conciergeWillSend'),
+ confirmText: translate('common.buttonConfirm'),
+ shouldShowCancelButton: false,
+ });
+ };
+
+ const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => {
+ if (isOffline) {
+ showOfflineModal();
+ return;
+ }
+
+ if (!moneyRequestReport) {
+ return;
+ }
+
+ showExportProgressModal().then((result) => {
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ clearSelectedTransactions(undefined, true);
+ });
+
+ queueExportSearchWithTemplate({
+ templateName,
+ templateType,
+ jsonQuery: '{}',
+ reportIDList: [moneyRequestReport.reportID],
+ transactionIDList,
+ policyID,
+ });
+ };
+
+ const exportSubmenuOptions: Record> = {
+ [CONST.REPORT.EXPORT_OPTIONS.DOWNLOAD_CSV]: {
+ text: translate('export.basicExport'),
+ icon: expensifyIcons.Table,
+ value: CONST.REPORT.EXPORT_OPTIONS.DOWNLOAD_CSV,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
+ onSelected: () => {
+ if (!moneyRequestReport) {
+ return;
+ }
+ if (isOffline) {
+ showOfflineModal();
+ return;
+ }
+ exportReportToCSV(
+ {
+ reportID: moneyRequestReport.reportID,
+ transactionIDList: transactionIDs,
+ },
+ () => {
+ showDownloadErrorModal();
+ },
+ translate,
+ );
+ },
+ },
+ [CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION]: {
+ text: translate('workspace.common.exportIntegrationSelected', {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ connectionName: connectedIntegrationFallback!,
+ }),
+ icon: getIntegrationIcon(connectedIntegration ?? connectedIntegrationFallback, expensifyIcons),
+ displayInDefaultIconColor: true,
+ additionalIconStyles: styles.integrationIcon,
+ value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
+ onSelected: () => {
+ if (!connectedIntegration || !moneyRequestReport) {
+ return;
+ }
+ if (isExported) {
+ triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION);
+ return;
+ }
+ exportToIntegration(moneyRequestReport.reportID, connectedIntegration);
+ },
+ },
+ [CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED]: {
+ text: translate('workspace.common.markAsExported'),
+ icon: getIntegrationIcon(connectedIntegration ?? connectedIntegrationFallback, expensifyIcons),
+ additionalIconStyles: styles.integrationIcon,
+ displayInDefaultIconColor: true,
+ value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
+ onSelected: () => {
+ if (!connectedIntegration || !moneyRequestReport) {
+ return;
+ }
+ if (isExported) {
+ triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED);
+ return;
+ }
+ markAsManuallyExported([moneyRequestReport.reportID ?? CONST.DEFAULT_NUMBER_ID], connectedIntegration);
+ },
+ },
+ };
+
+ for (const template of exportTemplates) {
+ exportSubmenuOptions[template.name] = {
+ text: template.name,
+ icon: expensifyIcons.Table,
+ value: template.templateName,
+ description: template.description,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
+ onSelected: () => beginExportWithTemplate(template.templateName, template.type, transactionIDs, template.policyID),
+ };
+ }
+
+ const secondaryExportActions = moneyRequestReport
+ ? getSecondaryExportReportActions(accountID, currentUserLogin ?? '', moneyRequestReport, bankAccountList, policy ?? undefined, exportTemplates)
+ : [];
+
+ const exportActionEntries: Record> & Pick> = {
+ [CONST.REPORT.SECONDARY_ACTIONS.EXPORT]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.EXPORT,
+ text: translate('common.export'),
+ backButtonText: translate('common.export'),
+ icon: expensifyIcons.Export,
+ rightIcon: expensifyIcons.ArrowRight,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT,
+ subMenuItems: secondaryExportActions.map((action) => exportSubmenuOptions[action as string]),
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF,
+ text: translate('common.downloadAsPDF'),
+ icon: expensifyIcons.Download,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DOWNLOAD_PDF,
+ onSelected: () => {
+ if (!moneyRequestReport?.reportID) {
+ return;
+ }
+ onPDFModalOpen?.();
+ exportReportToPDF({reportID: moneyRequestReport.reportID});
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.PRINT]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.PRINT,
+ text: translate('common.print'),
+ icon: expensifyIcons.Printer,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.PRINT,
+ onSelected: () => {
+ if (!moneyRequestReport) {
+ return;
+ }
+ openOldDotLink(CONST.OLDDOT_URLS.PRINTABLE_REPORT(moneyRequestReport.reportID));
+ },
+ },
+ };
+
+ return {
+ exportActionEntries,
+ secondaryExportActions,
+ beginExportWithTemplate,
+ showOfflineModal,
+ showDownloadErrorModal,
+ };
+}
+
+export default useExportActions;
+export type {UseExportActionsParams, UseExportActionsReturn};
diff --git a/src/hooks/useHoldRejectActions.ts b/src/hooks/useHoldRejectActions.ts
new file mode 100644
index 000000000000..8c383634720d
--- /dev/null
+++ b/src/hooks/useHoldRejectActions.ts
@@ -0,0 +1,123 @@
+import type {ValueOf} from 'type-fest';
+import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
+import type {SecondaryActionEntry} from '@components/MoneyReportHeaderActions/types';
+import type {RejectModalAction} from '@components/MoneyReportHeaderEducationalModals';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import Navigation from '@libs/Navigation/Navigation';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {changeMoneyRequestHoldStatus, isCurrentUserSubmitter, isDM} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import useGetIOUReportFromReportAction from './useGetIOUReportFromReportAction';
+import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
+import useLocalize from './useLocalize';
+import useNetwork from './useNetwork';
+import useOnyx from './useOnyx';
+import useTransactionThreadReport from './useTransactionThreadReport';
+
+type UseHoldRejectActionsParams = {
+ reportID: string | undefined;
+ onHoldEducationalOpen: () => void;
+ onRejectModalOpen: (action: RejectModalAction) => void;
+};
+
+type UseHoldRejectActionsReturn = Pick<
+ Record, SecondaryActionEntry>,
+ typeof CONST.REPORT.SECONDARY_ACTIONS.HOLD | typeof CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD | typeof CONST.REPORT.SECONDARY_ACTIONS.REJECT
+>;
+
+function useHoldRejectActions({reportID, onHoldEducationalOpen, onRejectModalOpen}: UseHoldRejectActionsParams): UseHoldRejectActionsReturn {
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
+ const {isDelegateAccessRestricted} = useDelegateNoAccessState();
+ const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
+
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'ThumbsDown'] as const);
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+ const {transactionThreadReport} = useTransactionThreadReport(reportID);
+
+ const [reportActionsForParent] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(moneyRequestReport?.reportID)}`, {canEvict: false});
+ const requestParentReportAction = transactionThreadReport?.parentReportActionID ? reportActionsForParent?.[transactionThreadReport.parentReportActionID] : undefined;
+
+ const {chatReport: chatIOUReport} = useGetIOUReportFromReportAction(requestParentReportAction);
+
+ const iouTransactionID = isMoneyRequestAction(requestParentReportAction)
+ ? (getOriginalMessage(requestParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID)
+ : CONST.DEFAULT_NUMBER_ID;
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${iouTransactionID}`);
+
+ const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION);
+ const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION);
+
+ const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport);
+ const isChatReportDM = isDM(chatReport);
+
+ return {
+ [CONST.REPORT.SECONDARY_ACTIONS.HOLD]: {
+ text: translate('iou.hold'),
+ icon: expensifyIcons.Stopwatch,
+ value: CONST.REPORT.SECONDARY_ACTIONS.HOLD,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.HOLD,
+ onSelected: () => {
+ if (!requestParentReportAction) {
+ throw new Error('Parent action does not exist');
+ }
+
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+
+ const isDismissed = isReportSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation;
+
+ if (isDismissed || isChatReportDM) {
+ changeMoneyRequestHoldStatus(requestParentReportAction, transaction, isOffline);
+ } else if (isReportSubmitter) {
+ onHoldEducationalOpen();
+ } else {
+ onRejectModalOpen(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD);
+ }
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD]: {
+ text: translate('iou.unhold'),
+ icon: expensifyIcons.Stopwatch,
+ value: CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REMOVE_HOLD,
+ onSelected: () => {
+ if (!requestParentReportAction) {
+ throw new Error('Parent action does not exist');
+ }
+
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+
+ changeMoneyRequestHoldStatus(requestParentReportAction, transaction, isOffline);
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.REJECT]: {
+ text: translate('common.reject'),
+ icon: expensifyIcons.ThumbsDown,
+ value: CONST.REPORT.SECONDARY_ACTIONS.REJECT,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REJECT,
+ onSelected: () => {
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+
+ if (moneyRequestReport?.reportID) {
+ Navigation.navigate(ROUTES.REJECT_EXPENSE_REPORT.getRoute(moneyRequestReport.reportID));
+ }
+ },
+ },
+ };
+}
+
+export default useHoldRejectActions;
+export type {UseHoldRejectActionsParams, UseHoldRejectActionsReturn};
diff --git a/src/hooks/useLifecycleActions.tsx b/src/hooks/useLifecycleActions.tsx
new file mode 100644
index 000000000000..574d24266242
--- /dev/null
+++ b/src/hooks/useLifecycleActions.tsx
@@ -0,0 +1,389 @@
+import {delegateEmailSelector} from '@selectors/Account';
+import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
+import type {ActionHandledType} from '@components/Modal/Global/HoldMenuModalWrapper';
+import {ModalActions} from '@components/Modal/Global/ModalContext';
+import type {SecondaryActionEntry} from '@components/MoneyReportHeaderActions/types';
+import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import Text from '@components/Text';
+import {search} from '@libs/actions/Search';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getValidConnectedIntegration} from '@libs/PolicyUtils';
+import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils';
+import {
+ getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils,
+ getNextApproverAccountID,
+ hasHeldExpenses as hasHeldExpensesReportUtils,
+ hasViolations as hasViolationsReportUtils,
+ isExported as isExportedUtils,
+ isReportOwner,
+ shouldBlockSubmitDueToStrictPolicyRules,
+} from '@libs/ReportUtils';
+import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils} from '@libs/TransactionUtils';
+import {cancelPayment} from '@userActions/IOU/PayMoneyRequest';
+import {approveMoneyRequest, reopenReport, retractReport, submitReport, unapproveExpenseReport} from '@userActions/IOU/ReportWorkflow';
+import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useConfirmModal from './useConfirmModal';
+import useConfirmPendingRTERAndProceed from './useConfirmPendingRTERAndProceed';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
+import useLocalize from './useLocalize';
+import useNetwork from './useNetwork';
+import useOnyx from './useOnyx';
+import usePaginatedReportActions from './usePaginatedReportActions';
+import usePermissions from './usePermissions';
+import useSearchShouldCalculateTotals from './useSearchShouldCalculateTotals';
+import useStrictPolicyRules from './useStrictPolicyRules';
+import useThemeStyles from './useThemeStyles';
+import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport';
+
+type UseLifecycleActionsParams = {
+ reportID: string | undefined;
+ startApprovedAnimation: () => void;
+ startSubmittingAnimation: () => void;
+ onHoldMenuOpen: (requestType: ActionHandledType, onConfirm?: () => void) => void;
+};
+
+type UseLifecycleActionsResult = {
+ actions: Record;
+ confirmApproval: (skipAnimation?: boolean) => void;
+ handleSubmitReport: (skipAnimation?: boolean) => void;
+ shouldBlockSubmit: boolean;
+ isBlockSubmitDueToPreventSelfApproval: boolean;
+};
+
+/**
+ * Provides report lifecycle transition actions (submit, approve, unapprove, cancel payment, retract, reopen)
+ * and their associated guards (delegate access, hold, pending RTER, strict policy rules).
+ */
+function useLifecycleActions({reportID, startApprovedAnimation, startSubmittingAnimation, onHoldMenuOpen}: UseLifecycleActionsParams): UseLifecycleActionsResult {
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+ const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`);
+ const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
+ const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
+ const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
+ const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
+
+ const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
+ const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
+
+ const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
+
+ const transactions = Object.values(reportTransactions);
+
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {accountID, email} = currentUserPersonalDetails;
+
+ const {areStrictPolicyRulesEnabled} = useStrictPolicyRules();
+ const {isBetaEnabled} = usePermissions();
+ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
+
+ const {isOffline} = useNetwork();
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {showConfirmModal} = useConfirmModal();
+ const {isDelegateAccessRestricted} = useDelegateNoAccessState();
+ const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
+
+ const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext();
+ const {clearSelectedTransactions} = useSearchActionsContext();
+ const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true);
+
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Send', 'ThumbsUp', 'CircularArrowBackwards', 'Clear']);
+
+ const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport);
+ const isSubmitterSameAsNextApprover =
+ isReportOwner(moneyRequestReport) && (nextApproverAccountID === moneyRequestReport?.ownerAccountID || moneyRequestReport?.managerID === moneyRequestReport?.ownerAccountID);
+ const isBlockSubmitDueToPreventSelfApproval = !!(isSubmitterSameAsNextApprover && policy?.preventSelfApproval);
+
+ const isBlockSubmitDueToStrictPolicyRules = shouldBlockSubmitDueToStrictPolicyRules(
+ moneyRequestReport?.reportID,
+ violations,
+ areStrictPolicyRulesEnabled,
+ accountID,
+ email ?? '',
+ transactions,
+ );
+
+ const shouldBlockSubmit = isBlockSubmitDueToStrictPolicyRules || isBlockSubmitDueToPreventSelfApproval;
+
+ const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? '');
+
+ const isExported = isExportedUtils(reportActions, moneyRequestReport);
+ const integrationNameFromExportMessage = isExported ? getIntegrationNameFromExportMessageUtils(reportActions) : null;
+
+ const connectedIntegration = getValidConnectedIntegration(policy);
+ const connectedIntegrationName = connectedIntegration
+ ? translate('workspace.accounting.connectionName', {
+ connectionName: connectedIntegration,
+ })
+ : '';
+
+ const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
+
+ const hasAnyPendingRTERViolation = hasAnyPendingRTERViolationTransactionUtils(transactions, allTransactionViolations, email ?? '', accountID, moneyRequestReport, policy);
+
+ const handleMarkPendingRTERTransactionsAsCash = () => {
+ markPendingRTERTransactionsAsCash(transactions, allTransactionViolations, reportActions);
+ };
+
+ const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash);
+
+ const confirmApproval = (skipAnimation = false) => {
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+ if (isAnyTransactionOnHold) {
+ onHoldMenuOpen(CONST.IOU.REPORT_ACTION_TYPE.APPROVE, skipAnimation ? undefined : () => startApprovedAnimation());
+ return;
+ }
+ if (!skipAnimation) {
+ startApprovedAnimation();
+ }
+ approveMoneyRequest({
+ expenseReport: moneyRequestReport,
+ expenseReportPolicy: policy,
+ policy,
+ currentUserAccountIDParam: accountID,
+ currentUserEmailParam: email ?? '',
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ expenseReportCurrentNextStepDeprecated: nextStep,
+ betas,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ full: true,
+ onApproved: () => {
+ if (skipAnimation) {
+ return;
+ }
+ startApprovedAnimation();
+ },
+ delegateEmail,
+ });
+ if (skipAnimation) {
+ clearSelectedTransactions(true);
+ }
+ };
+
+ const handleSubmitReport = (skipAnimation = false) => {
+ if (!moneyRequestReport || shouldBlockSubmit) {
+ return;
+ }
+
+ const doSubmit = () => {
+ submitReport({
+ expenseReport: moneyRequestReport,
+ policy,
+ currentUserAccountIDParam: accountID,
+ currentUserEmailParam: email ?? '',
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ expenseReportCurrentNextStepDeprecated: nextStep,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ onSubmitted: () => {
+ if (skipAnimation) {
+ return;
+ }
+ startSubmittingAnimation();
+ },
+ ownerBillingGracePeriodEnd,
+ delegateEmail,
+ });
+ if (currentSearchQueryJSON && !isOffline) {
+ search({
+ searchKey: currentSearchKey,
+ shouldCalculateTotals,
+ offset: 0,
+ queryJSON: currentSearchQueryJSON,
+ isOffline,
+ isLoading: !!currentSearchResults?.search?.isLoading,
+ });
+ }
+ if (skipAnimation) {
+ clearSelectedTransactions(true);
+ }
+ };
+
+ confirmPendingRTERAndProceed(doSubmit);
+ };
+
+ const actions: Record = {
+ [CONST.REPORT.SECONDARY_ACTIONS.SUBMIT]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.SUBMIT,
+ text: translate('common.submit'),
+ icon: expensifyIcons.Send,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.SUBMIT,
+ onSelected: () => {
+ if (!moneyRequestReport) {
+ return;
+ }
+ confirmPendingRTERAndProceed(() => {
+ submitReport({
+ expenseReport: moneyRequestReport,
+ policy,
+ currentUserAccountIDParam: accountID,
+ currentUserEmailParam: email ?? '',
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ expenseReportCurrentNextStepDeprecated: nextStep,
+ userBillingGracePeriodEnds,
+ amountOwed,
+ ownerBillingGracePeriodEnd,
+ delegateEmail,
+ });
+ });
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.APPROVE]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.APPROVE,
+ text: translate('iou.approve'),
+ icon: expensifyIcons.ThumbsUp,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.APPROVE,
+ onSelected: confirmApproval,
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE,
+ text: translate('iou.unapprove'),
+ icon: expensifyIcons.CircularArrowBackwards,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.UNAPPROVE,
+ onSelected: async () => {
+ if (isDelegateAccessRestricted) {
+ showDelegateNoAccessModal();
+ return;
+ }
+
+ if (isExported) {
+ const unapproveWarningText = (
+
+ {translate('iou.headsUp')}{' '}
+ {translate('iou.unapproveWithIntegrationWarning', connectedIntegrationName)}
+
+ );
+
+ const result = await showConfirmModal({
+ title: translate('iou.unapproveReport'),
+ prompt: unapproveWarningText,
+ confirmText: translate('iou.unapproveReport'),
+ cancelText: translate('common.cancel'),
+ danger: true,
+ });
+
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ }
+
+ unapproveExpenseReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, delegateEmail);
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT,
+ text: translate('iou.cancelPayment'),
+ icon: expensifyIcons.Clear,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.CANCEL_PAYMENT,
+ onSelected: async () => {
+ const result = await showConfirmModal({
+ title: translate('iou.cancelPayment'),
+ prompt: translate('iou.cancelPaymentConfirmation'),
+ confirmText: translate('iou.cancelPayment'),
+ cancelText: translate('common.dismiss'),
+ danger: true,
+ });
+
+ if (result.action !== ModalActions.CONFIRM || !chatReport) {
+ return;
+ }
+
+ cancelPayment(moneyRequestReport, chatReport, policy, isASAPSubmitBetaEnabled, accountID, email ?? '', hasViolations);
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.RETRACT]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.RETRACT,
+ text: translate('iou.retract'),
+ icon: expensifyIcons.CircularArrowBackwards,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.RETRACT,
+ onSelected: async () => {
+ if (isExported) {
+ const reopenExportedReportWarningText = (
+
+ {translate('iou.headsUp')}
+
+ {translate('iou.reopenExportedReportConfirmation', {
+ connectionName: integrationNameFromExportMessage ?? '',
+ })}
+
+
+ );
+
+ const result = await showConfirmModal({
+ title: translate('iou.reopenReport'),
+ prompt: reopenExportedReportWarningText,
+ confirmText: translate('iou.reopenReport'),
+ cancelText: translate('common.cancel'),
+ danger: true,
+ });
+
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ }
+
+ retractReport(moneyRequestReport, chatReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, delegateEmail);
+ },
+ },
+ [CONST.REPORT.SECONDARY_ACTIONS.REOPEN]: {
+ value: CONST.REPORT.SECONDARY_ACTIONS.REOPEN,
+ text: translate('iou.retract'),
+ icon: expensifyIcons.CircularArrowBackwards,
+ sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.REOPEN,
+ onSelected: async () => {
+ if (isExported) {
+ const reopenExportedReportWarningText = (
+
+ {translate('iou.headsUp')}
+
+ {translate('iou.reopenExportedReportConfirmation', {
+ connectionName: integrationNameFromExportMessage ?? '',
+ })}
+
+
+ );
+
+ const result = await showConfirmModal({
+ title: translate('iou.reopenReport'),
+ prompt: reopenExportedReportWarningText,
+ confirmText: translate('iou.reopenReport'),
+ cancelText: translate('common.cancel'),
+ danger: true,
+ });
+
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ }
+
+ reopenReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, chatReport);
+ },
+ },
+ };
+
+ return {
+ actions,
+ confirmApproval,
+ handleSubmitReport,
+ shouldBlockSubmit,
+ isBlockSubmitDueToPreventSelfApproval,
+ };
+}
+
+export default useLifecycleActions;
+export type {UseLifecycleActionsParams, UseLifecycleActionsResult};
diff --git a/src/hooks/useReportPrimaryAction.ts b/src/hooks/useReportPrimaryAction.ts
new file mode 100644
index 000000000000..00cefb3f199d
--- /dev/null
+++ b/src/hooks/useReportPrimaryAction.ts
@@ -0,0 +1,59 @@
+import type {ValueOf} from 'type-fest';
+import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getReportPrimaryAction} from '@libs/ReportPrimaryActionUtils';
+import {isTransactionPendingDelete} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useOnyx from './useOnyx';
+import useReportIsArchived from './useReportIsArchived';
+import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport';
+import useTransactionThreadReport from './useTransactionThreadReport';
+
+function useReportPrimaryAction(reportID: string | undefined): ValueOf | '' {
+ const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning} = usePaymentAnimationsContext();
+ const {login: currentUserLogin, accountID} = useCurrentUserPersonalDetails();
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(moneyRequestReport?.reportID)}`);
+ const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${getNonEmptyStringOnyxID(moneyRequestReport?.reportID)}`);
+ const [invoiceReceiverPolicy] = useOnyx(
+ `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`,
+ );
+
+ const {reportActions} = useTransactionThreadReport(reportID);
+ const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
+
+ const isChatReportArchived = useReportIsArchived(chatReport?.reportID);
+
+ if (isPaidAnimationRunning || isApprovedAnimationRunning) {
+ return CONST.REPORT.PRIMARY_ACTIONS.PAY;
+ }
+ if (isSubmittingAnimationRunning) {
+ return CONST.REPORT.PRIMARY_ACTIONS.SUBMIT;
+ }
+
+ const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => !isTransactionPendingDelete(t));
+
+ return getReportPrimaryAction({
+ currentUserLogin: currentUserLogin ?? '',
+ currentUserAccountID: accountID,
+ report: moneyRequestReport,
+ chatReport,
+ reportTransactions: nonPendingDeleteTransactions,
+ violations,
+ bankAccountList,
+ policy,
+ reportNameValuePairs,
+ reportActions,
+ reportMetadata,
+ isChatReportArchived,
+ invoiceReceiverPolicy,
+ });
+}
+
+export default useReportPrimaryAction;
diff --git a/src/hooks/useTransactionThreadReport.ts b/src/hooks/useTransactionThreadReport.ts
new file mode 100644
index 000000000000..f8d64f58642b
--- /dev/null
+++ b/src/hooks/useTransactionThreadReport.ts
@@ -0,0 +1,40 @@
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils';
+import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID} from '@libs/ReportActionsUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useNetwork from './useNetwork';
+import useOnyx from './useOnyx';
+import usePaginatedReportActions from './usePaginatedReportActions';
+import useReportTransactionsCollection from './useReportTransactionsCollection';
+
+/**
+ * Derives the single-transaction thread report ID and report for a money request report.
+ *
+ * This pattern is repeated across multiple hooks and components that need to know
+ * whether a report has a single transaction thread (and access its data).
+ */
+function useTransactionThreadReport(reportID: string | undefined) {
+ const {isOffline} = useNetwork();
+
+ const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);
+
+ const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
+ const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
+
+ const allReportTransactions = useReportTransactionsCollection(reportID);
+ const nonDeletedTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true);
+ const visibleTransactions = nonDeletedTransactions?.filter((t) => isOffline || t.pendingAction !== 'delete');
+ const reportTransactionIDs = visibleTransactions?.map((t) => t.transactionID);
+
+ const transactionThreadReportID = getOneTransactionThreadReportID(moneyRequestReport, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);
+ const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionThreadReportID)}`);
+
+ return {
+ transactionThreadReportID,
+ transactionThreadReport,
+ reportActions,
+ };
+}
+
+export default useTransactionThreadReport;
diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts
index c3e039492aa4..ab1e3c9d9d7a 100644
--- a/src/libs/ReportPrimaryActionUtils.ts
+++ b/src/libs/ReportPrimaryActionUtils.ts
@@ -74,9 +74,6 @@ type GetReportPrimaryActionParams = {
reportMetadata?: OnyxEntry;
isChatReportArchived: boolean;
invoiceReceiverPolicy?: Policy;
- isPaidAnimationRunning?: boolean;
- isApprovedAnimationRunning?: boolean;
- isSubmittingAnimationRunning?: boolean;
};
type IsPrimaryPayActionParams = {
@@ -461,9 +458,6 @@ function getReportPrimaryAction(params: GetReportPrimaryActionParams): ValueOf