diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c924ae07be3c..8ba61ea9b986 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1,19 +1,11 @@ import {useRoute} from '@react-navigation/native'; -import {isUserValidatedSelector} from '@selectors/Account'; -import {shouldFailAllRequestsSelector} from '@selectors/Network'; -import {hasSeenTourSelector} from '@selectors/Onboarding'; import {getArchiveReason} from '@selectors/Report'; -import {validTransactionDraftsSelector} from '@selectors/TransactionDraft'; -import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy'; -import useDeleteTransactions from '@hooks/useDeleteTransactions'; -import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; -import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -21,7 +13,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; 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'; @@ -32,24 +23,15 @@ import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsAction 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 {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 {createTransactionThreadReport, deleteAppReport, downloadReportPDF, exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report'; -import {getExportTemplates, queueExportSearchWithTemplate, search} from '@libs/actions/Search'; -import initSplitExpense from '@libs/actions/SplitExpenses'; -import {setNameValuePair} from '@libs/actions/User'; +import {queueExportSearchWithTemplate, search} from '@libs/actions/Search'; import {isPersonalCard} from '@libs/CardUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import getPlatform from '@libs/getPlatform'; -import {getExistingTransactionID} from '@libs/IOUUtils'; import Log from '@libs/Log'; -import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; -import Navigation from '@libs/Navigation/Navigation'; +import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import { @@ -59,22 +41,14 @@ import { buildOptimisticNextStepForStrictPolicyRuleViolations, getReportNextStep, } from '@libs/NextStepUtils'; -import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; -import {selectPaymentType} from '@libs/PaymentUtils'; -import {getConnectedIntegration, getValidConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {getIOUActionForReportID, getOriginalMessage, getReportAction, hasPendingDEWApprove, hasPendingDEWSubmit, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {getAllExpensesToHoldIfApplicable, getReportPrimaryAction, isMarkAsResolvedAction} from '@libs/ReportPrimaryActionUtils'; -import {getSecondaryExportReportActions, getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils'; +import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; +import {getOriginalMessage, hasPendingDEWApprove, hasPendingDEWSubmit, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {getReportPrimaryAction, isMarkAsResolvedAction} from '@libs/ReportPrimaryActionUtils'; import { - changeMoneyRequestHoldStatus, - generateReportID, - getAddExpenseDropdownOptions, getAllReportActionsErrorsAndReportActionThatRequiresAttention, - getIntegrationIcon, getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils, getNextApproverAccountID, getNonHeldAndFullAmount, - getPolicyExpenseChat, getReasonAndReportActionThatRequiresAttention, getTransactionsWithReceipts, hasHeldExpenses as hasHeldExpensesReportUtils, @@ -82,53 +56,25 @@ import { hasUpdatedTotal, hasViolations as hasViolationsReportUtils, isAllowedToApproveExpenseReport, - isCurrentUserSubmitter, - isDM, isExported as isExportedUtils, isInvoiceReport as isInvoiceReportUtil, isOpenExpenseReport, isProcessingReport, isReportOwner, - navigateOnDeleteExpense, - navigateToDetailsPage, - rejectMoneyRequestReason, shouldBlockSubmitDueToStrictPolicyRules, } from '@libs/ReportUtils'; -import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import { allHavePendingRTERViolation, - getOriginalTransactionWithSplitInfo, - hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, - isDuplicate, isExpensifyCardTransaction, isPayAtEndExpense as isPayAtEndExpenseTransactionUtils, isPending, - isPerDiemRequest, isScanning, isTransactionPendingDelete, shouldShowBrokenConnectionViolationForMultipleTransactions, } from '@libs/TransactionUtils'; -import type {ExportType} from '@pages/inbox/report/ReportDetailsExportPage'; import variables from '@styles/variables'; -import { - approveMoneyRequest, - canApproveIOU, - cancelPayment, - canIOUBePaid as canIOUBePaidAction, - dismissRejectUseExplanation, - getNavigationUrlOnMoneyRequestDelete, - markRejectViolationAsResolved, - payInvoice, - payMoneyRequest, - reopenReport, - retractReport, - startMoneyRequest, - submitReport, - unapproveExpenseReport, -} from '@userActions/IOU'; -import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report'; -import {markAsCash as markAsCashAction} from '@userActions/Transaction'; +import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidAction, payInvoice, payMoneyRequest} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -136,39 +82,25 @@ import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type IconAsset from '@src/types/utils/IconAsset'; -import ActivityIndicator from './ActivityIndicator'; -import AnimatedSubmitButton from './AnimatedSubmitButton'; import BrokenConnectionDescription from './BrokenConnectionDescription'; -import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import type {DropdownOption} from './ButtonWithDropdownMenu/types'; -import ConfirmModal from './ConfirmModal'; -import DecisionModal from './DecisionModal'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from './DelegateNoAccessModalProvider'; -import Header from './Header'; import HeaderLoadingBar from './HeaderLoadingBar'; import HeaderWithBackButton from './HeaderWithBackButton'; -import HoldOrRejectEducationalModal from './HoldOrRejectEducationalModal'; -import HoldSubmitterEducationalModal from './HoldSubmitterEducationalModal'; import Icon from './Icon'; -import {KYCWallContext} from './KYCWall/KYCWallContext'; import type {PaymentMethod} from './KYCWall/types'; -import Modal from './Modal'; import {ModalActions} from './Modal/Global/ModalContext'; -import MoneyReportHeaderKYCDropdown from './MoneyReportHeaderKYCDropdown'; +import type {MoneyReportHeaderContextType} from './MoneyReportHeaderContext'; +import {MoneyReportHeaderProvider} from './MoneyReportHeaderContext'; +import MoneyReportHeaderModals from './MoneyReportHeaderModals'; +import MoneyReportHeaderPrimaryAction from './MoneyReportHeaderPrimaryAction'; +import MoneyReportHeaderSecondaryActions from './MoneyReportHeaderSecondaryActions'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; import MoneyReportHeaderStatusBarSkeleton from './MoneyReportHeaderStatusBarSkeleton'; import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import MoneyRequestReportNavigation from './MoneyRequestReportView/MoneyRequestReportNavigation'; -import {usePersonalDetails} from './OnyxListItemProvider'; -import type {PopoverMenuItem} from './PopoverMenu'; -import {PressableWithFeedback} from './Pressable'; -import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; -import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import {useSearchActionsContext, useSearchStateContext} from './Search/SearchContext'; -import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton'; -import Text from './Text'; type MoneyReportHeaderProps = { /** The report currently being looked at */ @@ -203,6 +135,42 @@ function MoneyReportHeader({ shouldDisplayBackButton = false, onBackButtonPress, }: MoneyReportHeaderProps) { + const shouldTrackRenderPerformance = __DEV__; + const renderCountRef = useRef(0); + renderCountRef.current += 1; + const renderStartTime = shouldTrackRenderPerformance ? performance.now() : 0; + let previousRenderCheckpointTime = renderStartTime; + const renderPerformanceCheckpoints: Array<{phase: string; deltaMs: number; elapsedMs: number}> = []; + + const trackRenderPhase = (phase: string) => { + if (!shouldTrackRenderPerformance) { + return; + } + + const now = performance.now(); + renderPerformanceCheckpoints.push({ + phase, + deltaMs: Number((now - previousRenderCheckpointTime).toFixed(2)), + elapsedMs: Number((now - renderStartTime).toFixed(2)), + }); + previousRenderCheckpointTime = now; + }; + + const logRenderPerformance = (renderPath: 'default' | 'mobileSelectionMode') => { + if (!shouldTrackRenderPerformance) { + return; + } + + const totalDurationMs = Number((performance.now() - renderStartTime).toFixed(2)); + Log.info('[MoneyReportHeader][Perf] Render breakdown', false, { + renderPath, + reportID: moneyRequestReport?.reportID, + renderCount: renderCountRef.current, + totalDurationMs, + checkpoints: renderPerformanceCheckpoints, + }); + }; + // 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 const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); @@ -214,76 +182,20 @@ function MoneyReportHeader({ | PlatformStackRouteProp >(); const {login: currentUserLogin, accountID, email} = useCurrentUserPersonalDetails(); - const personalDetails = usePersonalDetails(); - const defaultExpensePolicy = useDefaultExpensePolicy(); - const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id); - const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`); const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); - const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector}); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); - const [reportPDFFilename] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDF_FILENAME}${moneyRequestReport?.reportID}`) ?? null; const [session] = useOnyx(ONYXKEYS.SESSION); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); 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 expensifyIcons = useMemoizedLazyExpensifyIcons([ - 'Buildings', - 'Plus', - 'Hourglass', - 'Cash', - 'Box', - 'Stopwatch', - 'Flag', - 'CreditCardHourglass', - 'Send', - 'Clear', - 'ReceiptScan', - 'ThumbsUp', - 'CircularArrowBackwards', - 'ArrowSplit', - 'ArrowCollapse', - 'Workflows', - 'Trashcan', - 'ArrowRight', - 'ThumbsDown', - 'Table', - 'Info', - 'Export', - 'Download', - 'XeroSquare', - 'QBOSquare', - 'NetSuiteSquare', - 'IntacctSquare', - 'QBDSquare', - 'CertiniaSquare', - 'Feed', - 'Close', - 'Location', - 'ReceiptPlus', - 'ExpenseCopy', - 'Checkmark', - 'ReportCopy', - ] as const); - 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 encryptedAuthToken = session?.encryptedAuthToken ?? ''; + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Hourglass', 'Box', 'Stopwatch', 'Flag', 'CreditCardHourglass', 'ReceiptScan'] as const); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${moneyRequestReport?.reportID}`); - const exportTemplates = useMemo( - () => getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy), - [integrationsExportTemplates, csvExportLayouts, policy, translate], - ); + const {translate} = useLocalize(); const {areStrictPolicyRulesEnabled} = useStrictPolicyRules(); - const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const requestParentReportAction = useMemo(() => { if (!reportActions || !transactionThreadReport?.parentReportActionID) { @@ -292,8 +204,6 @@ function MoneyReportHeader({ return reportActions.find((action): action is OnyxTypes.ReportAction => action.reportActionID === transactionThreadReport.parentReportActionID); }, [reportActions, transactionThreadReport?.parentReportActionID]); - const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(requestParentReportAction); - const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID); const {transactions, nonPendingDeleteTransactions} = useMemo(() => { @@ -318,37 +228,29 @@ function MoneyReportHeader({ const isBlockSubmitDueToStrictPolicyRules = useMemo(() => { return shouldBlockSubmitDueToStrictPolicyRules(moneyRequestReport?.reportID, violations, areStrictPolicyRulesEnabled, accountID, email ?? '', transactions); }, [moneyRequestReport?.reportID, violations, areStrictPolicyRulesEnabled, accountID, email, transactions]); - const shouldBlockSubmit = isBlockSubmitDueToStrictPolicyRules || isBlockSubmitDueToPreventSelfApproval; + const shouldBlockSubmit = isBlockSubmitDueToStrictPolicyRules || !!isBlockSubmitDueToPreventSelfApproval; 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); + return getIntegrationNameFromExportMessageUtils(reportActions) ?? null; }, [isExported, reportActions]); const transactionViolations = useTransactionViolations(transaction?.transactionID); - const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); - const [isPDFModalVisible, setIsPDFModalVisible] = useState(false); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - 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 [cardList] = useOnyx(ONYXKEYS.CARD_LIST); const {isBetaEnabled} = usePermissions(); @@ -356,53 +258,21 @@ function MoneyReportHeader({ const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); const isDEWPolicy = isDEWBetaEnabled && hasDynamicExternalWorkflow(policy); const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? ''); - const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations); - const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && defaultExpensePolicy?.id !== policy?.id; - const [exportModalStatus, setExportModalStatus] = useState(null); const {showConfirmModal} = useConfirmModal(); const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning, startAnimation, stopAnimation, startApprovedAnimation, startSubmittingAnimation} = usePaymentAnimations(); const styles = useThemeStyles(); const theme = useTheme(); const {isOffline} = useNetwork(); - const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [isDuplicateActive, temporarilyDisableDuplicateAction] = useThrottledButtonState(); - const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); - const [paymentType, setPaymentType] = useState(); - const [requestType, setRequestType] = useState(); const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy); const policyType = policy?.type; - const connectedIntegration = getValidConnectedIntegration(policy); - const connectedIntegrationFallback = getConnectedIntegration(policy); const hasScanningReceipt = getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => isScanning(t)); 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]); - // eslint-disable-next-line rulesdir/no-negated-variables - const canTriggerAutomaticPDFDownload = useRef(false); - const hasFinishedPDFDownload = reportPDFFilename && reportPDFFilename !== CONST.REPORT_DETAILS_MENU_ITEM.ERROR; - - 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}); - - useEffect(() => { - canTriggerAutomaticPDFDownload.current = isPDFModalVisible; - }, [isPDFModalVisible]); - - const messagePDF = useMemo(() => { - if (reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR) { - return translate('reportDetailsPage.errorPDF'); - } - if (!hasFinishedPDFDownload) { - return translate('reportDetailsPage.waitForPDF'); - } - return translate('reportDetailsPage.successPDF'); - }, [reportPDFFilename, hasFinishedPDFDownload, translate]); // Check if there is pending rter violation in all transactionViolations with given transactionIDs. // wrapped in useMemo to avoid unnecessary re-renders and for better performance (array operation inside of function) @@ -428,25 +298,28 @@ function MoneyReportHeader({ const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); - const [rateErrorModalVisible, setRateErrorModalVisible] = useState(false); - const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); - const [duplicatePerDiemErrorModalVisible, setDuplicatePerDiemErrorModalVisible] = useState(false); - const [rejectModalAction, setRejectModalAction] = useState | null>(null); - - const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchStateContext(); - const {removeTransaction, clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {clearSelectedTransactions} = useSearchActionsContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); - const [shouldFailAllRequests] = useOnyx(ONYXKEYS.NETWORK, {selector: shouldFailAllRequestsSelector}); - const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP(); const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout; - const [offlineModalVisible, setOfflineModalVisible] = useState(false); + // Modal triggers - registered by MoneyReportHeaderModals via onRegisterTriggers callback + const modalTriggersRef = useRef<{ + showHoldMenu: MoneyReportHeaderContextType['showHoldMenu']; + showDownloadError: MoneyReportHeaderContextType['showDownloadError']; + showExportDownloadError: MoneyReportHeaderContextType['showExportDownloadError']; + showOfflineModal: MoneyReportHeaderContextType['showOfflineModal']; + showPDFModal: MoneyReportHeaderContextType['showPDFModal']; + showHoldEducationalModal: MoneyReportHeaderContextType['showHoldEducationalModal']; + setRejectModalAction: MoneyReportHeaderContextType['setRejectModalAction']; + showRateErrorModal: MoneyReportHeaderContextType['showRateErrorModal']; + showDuplicatePerDiemErrorModal: MoneyReportHeaderContextType['showDuplicatePerDiemErrorModal']; + } | null>(null); + + trackRenderPhase('subscriptions and core derived state'); const showExportProgressModal = useCallback(() => { return showConfirmModal({ @@ -460,7 +333,7 @@ function MoneyReportHeader({ const beginExportWithTemplate = useCallback( (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => { if (isOffline) { - setOfflineModalVisible(true); + modalTriggersRef.current?.showOfflineModal(); return; } @@ -496,8 +369,8 @@ function MoneyReportHeader({ reportActions, allTransactionsLength: transactions.length, session, - onExportFailed: () => setIsDownloadErrorModalVisible(true), - onExportOffline: () => setOfflineModalVisible(true), + onExportFailed: () => modalTriggersRef.current?.showExportDownloadError(), + onExportOffline: () => modalTriggersRef.current?.showOfflineModal(), policy, beginExportWithTemplate: (templateName, templateType, transactionIDList, policyID) => beginExportWithTemplate(templateName, templateType, transactionIDList, policyID), isOnSearch, @@ -568,14 +441,10 @@ function MoneyReportHeader({ const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - 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 confirmPayment = useCallback( @@ -583,17 +452,10 @@ function MoneyReportHeader({ if (!type || !chatReport) { return; } - setPaymentType(type); - setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY); if (isDelegateAccessRestricted) { showDelegateNoAccessModal(); } else if (isAnyTransactionOnHold) { - if (getPlatform() === CONST.PLATFORM.IOS) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => setIsHoldMenuVisible(true)); - } else { - setIsHoldMenuVisible(true); - } + modalTriggersRef.current?.showHoldMenu(type, CONST.IOU.REPORT_ACTION_TYPE.PAY); } else if (isInvoiceReport) { startAnimation(); payInvoice({ @@ -623,7 +485,6 @@ function MoneyReportHeader({ activePolicy, policy, betas, - userBillingGraceEndPeriods, }); if (currentSearchQueryJSON && !isOffline) { search({ @@ -658,7 +519,6 @@ function MoneyReportHeader({ shouldCalculateTotals, currentSearchResults?.search?.isLoading, betas, - userBillingGraceEndPeriods, ], ); @@ -680,84 +540,16 @@ function MoneyReportHeader({ showDWEModal(); return; } - setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE); if (isDelegateAccessRestricted) { showDelegateNoAccessModal(); } else if (isAnyTransactionOnHold) { - setIsHoldMenuVisible(true); + modalTriggersRef.current?.showHoldMenu(undefined, CONST.IOU.REPORT_ACTION_TYPE.APPROVE); } else { startApprovedAnimation(); approveMoneyRequest(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, betas, userBillingGraceEndPeriods, true); } }; - const markAsCash = useCallback(() => { - if (!requestParentReportAction) { - return; - } - const reportID = transactionThreadReport?.reportID; - - if (!iouTransactionID || !reportID) { - return; - } - markAsCashAction(iouTransactionID, reportID, transactionViolations); - }, [iouTransactionID, requestParentReportAction, transactionThreadReport?.reportID, transactionViolations]); - - 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, - }); - } - }, - [ - activePolicyExpenseChat, - activePolicyID, - allPolicyCategories, - transactionDrafts, - defaultExpensePolicy, - draftTransactionIDs, - introSelected, - isASAPSubmitBetaEnabled, - quickAction, - policyRecentlyUsedCurrencies, - policy?.id, - isSelfTourViewed, - betas, - personalDetails, - recentWaypoints, - ], - ); - const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( { - const duplicateTransaction = transactionsList.find((reportTransaction) => - isDuplicate( - reportTransaction, - email ?? '', - accountID, - moneyRequestReport, - policy, - allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + reportTransaction.transactionID], - ), - ); - if (!duplicateTransaction) { - return null; - } - - return getThreadReportIDsForTransactions(allReportActions, [duplicateTransaction]).at(0); - }; - const statusBarProps = getStatusBarProps(); - const dismissModalAndUpdateUseHold = () => { - setIsHoldEducationalModalVisible(false); - setNameValuePair(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, true, false, !shouldFailAllRequests); - if (requestParentReportAction) { - changeMoneyRequestHoldStatus(requestParentReportAction); - } - }; - - const dismissRejectModalBasedOnAction = () => { - if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD) { - dismissRejectUseExplanation(); - if (requestParentReportAction) { - changeMoneyRequestHoldStatus(requestParentReportAction); - } - } else if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK) { - dismissRejectUseExplanation(); - if (moneyRequestReport?.reportID) { - Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS.getRoute({reportID: moneyRequestReport.reportID})); - } - } else { - dismissRejectUseExplanation(); - if (requestParentReportAction) { - rejectMoneyRequestReason(requestParentReportAction); - } - } - setRejectModalAction(null); - }; - const primaryAction = useMemo(() => { return getReportPrimaryAction({ currentUserLogin: currentUserLogin ?? '', @@ -908,753 +654,13 @@ function MoneyReportHeader({ bankAccountList, ]); - const confirmExport = useCallback(() => { - setExportModalStatus(null); - if (!moneyRequestReport?.reportID || !connectedIntegration) { - return; - } - if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { - exportToIntegration(moneyRequestReport?.reportID, connectedIntegration); - } else if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { - markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration); - } - }, [connectedIntegration, exportModalStatus, moneyRequestReport?.reportID]); - const getAmount = (actionType: ValueOf) => ({ formattedAmount: getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, actionType), }); 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 addExpenseDropdownOptions = useMemo( - () => getAddExpenseDropdownOptions(translate, expensifyIcons, moneyRequestReport?.reportID, policy, userBillingGraceEndPeriods, undefined, undefined, lastDistanceExpenseType), - [moneyRequestReport?.reportID, policy, userBillingGraceEndPeriods, lastDistanceExpenseType, expensifyIcons, translate], - ); - - 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) { - setOfflineModalVisible(true); - return; - } - exportReportToCSV( - {reportID: moneyRequestReport.reportID, transactionIDList: transactionIDs}, - () => { - setDownloadErrorModalVisible(true); - }, - translate, - ); - }, - }, - [CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION]: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - text: translate('workspace.common.exportIntegrationSelected', {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) { - setExportModalStatus(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) { - setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED); - return; - } - markAsManuallyExported(moneyRequestReport?.reportID, 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, - ]); - - const primaryActionsImplementation = { - [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: ( - { - if (!moneyRequestReport || shouldBlockSubmit) { - return; - } - if (hasDynamicExternalWorkflow(policy) && !isDEWBetaEnabled) { - showDWEModal(); - return; - } - startSubmittingAnimation(); - submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods); - if (currentSearchQueryJSON && !isOffline) { - search({ - searchKey: currentSearchKey, - shouldCalculateTotals, - offset: 0, - queryJSON: currentSearchQueryJSON, - isOffline, - isLoading: !!currentSearchResults?.search?.isLoading, - }); - } - }} - isSubmittingAnimationRunning={isSubmittingAnimationRunning} - onAnimationFinish={stopAnimation} - isDisabled={shouldBlockSubmit} - /> - ), - [CONST.REPORT.PRIMARY_ACTIONS.APPROVE]: ( -