From 1198d5642c30f43479737b804379f7e054c685a0 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 18 Jun 2026 17:42:14 +0200 Subject: [PATCH 1/5] perf: stabilized chatReport into MoneyRequestReportPreview --- .../MoneyRequestReportPreviewContent.tsx | 1 + .../MoneyRequestReportPreview/PayActionButton.tsx | 6 ++++-- .../ReportPreviewActionButton.tsx | 7 +++++-- .../ReportActionItem/MoneyRequestReportPreview/index.tsx | 3 ++- .../ReportActionItem/MoneyRequestReportPreview/types.ts | 7 ++++++- .../ReportActionItem/TransactionPreview/index.tsx | 3 +-- .../ReportActionItem/TransactionPreview/types.ts | 5 ++++- src/pages/inbox/report/ReportActionItem.tsx | 1 + .../inbox/report/actionContents/ActionContentRouter.tsx | 6 ++++++ .../report/actionContents/ChatTransactionPreview.tsx | 6 +++++- tests/ui/MoneyRequestReportPreview.test.tsx | 9 +++++---- 11 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 3193f0e8d29f..8cb36cff9ddf 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -742,6 +742,7 @@ function MoneyRequestReportPreviewContent({ ; isPaidAnimationRunning: boolean; isApprovedAnimationRunning: boolean; stopAnimation: () => void; @@ -49,6 +51,7 @@ type PayActionButtonProps = { function PayActionButton({ iouReportID, chatReportID, + chatReport, isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, @@ -74,7 +77,6 @@ function PayActionButton({ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const activePolicy = usePolicy(activePolicyID); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`); const chatReportPolicy = usePolicy(chatReport?.policyID); const [invoiceReceiverPolicy] = useOnyx( diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx index 6bc93cac0698..6dcbe324ac35 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import ExportWithDropdownMenu from '@components/ReportActionItem/ExportWithDropdownMenu'; @@ -21,7 +22,7 @@ import {canIOUBePaid as canIOUBePaidIOUActions} from '@userActions/IOU/ReportWor import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'; -import type {Transaction} from '@src/types/onyx'; +import type {Report, Transaction} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ApproveActionButton from './ApproveActionButton'; import PayActionButton from './PayActionButton'; @@ -30,6 +31,7 @@ import SubmitActionButton from './SubmitActionButton'; type ReportPreviewActionButtonProps = { iouReportID: string | undefined; chatReportID: string | undefined; + chatReport: OnyxEntry; isPaidAnimationRunning: boolean; isApprovedAnimationRunning: boolean; isSubmittingAnimationRunning: boolean; @@ -47,6 +49,7 @@ type ReportPreviewActionButtonProps = { function ReportPreviewActionButton({ iouReportID, chatReportID, + chatReport, isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning, @@ -67,7 +70,6 @@ function ReportPreviewActionButton({ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'ReceiptPlus', 'Plus']); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`); const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`); @@ -169,6 +171,7 @@ function ReportPreviewActionButton({ = ({item}) => ( ; + /** The active IOUReport, used for Onyx subscription */ iouReportID: string | undefined; @@ -53,7 +59,6 @@ type MoneyRequestReportPreviewProps = { }; type MoneyRequestReportPreviewContentOnyxProps = { - chatReport: OnyxEntry; invoiceReceiverPolicy: OnyxEntry; iouReport: OnyxEntry; transactions: Transaction[]; diff --git a/src/components/ReportActionItem/TransactionPreview/index.tsx b/src/components/ReportActionItem/TransactionPreview/index.tsx index 0274f83fb345..fc9fba4e9a3d 100644 --- a/src/components/ReportActionItem/TransactionPreview/index.tsx +++ b/src/components/ReportActionItem/TransactionPreview/index.tsx @@ -31,11 +31,10 @@ import type {TransactionPreviewProps} from './types'; function TransactionPreview(props: TransactionPreviewProps) { const {translate} = useLocalize(); const {convertToDisplayString} = useCurrencyListActions(); - const {action, chatReportID, reportID, transactionID: transactionIDFromProps, onPreviewPressed, shouldHighlight, reportPreviewAction, contextAction} = props; + const {action, chatReport, chatReportID, reportID, transactionID: transactionIDFromProps, onPreviewPressed, shouldHighlight, reportPreviewAction, contextAction} = props; const {anchor: contextMenuAnchorRef, shouldDisplayContextMenu, originalReportID} = useShowContextMenuState(); const {checkIfContextMenuActive} = useShowContextMenuActions(); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const route = useRoute>(); const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action); const transactionID = transactionIDFromProps ?? (isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : undefined); diff --git a/src/components/ReportActionItem/TransactionPreview/types.ts b/src/components/ReportActionItem/TransactionPreview/types.ts index 008a178bc2bf..b98d056ba76c 100644 --- a/src/components/ReportActionItem/TransactionPreview/types.ts +++ b/src/components/ReportActionItem/TransactionPreview/types.ts @@ -16,6 +16,9 @@ type TransactionPreviewProps = { /** The associated chatReport */ chatReportID: string | undefined; + /** The associated chat report, passed from the parent to avoid subscribing to chat heartbeat fields in each preview */ + chatReport: OnyxEntry; + /** The ID of the current report */ reportID: string | undefined; @@ -101,7 +104,7 @@ type TransactionPreviewContentProps = { violations: TransactionViolations; /** Holds the chat report entry from Onyx */ - chatReport?: Report; + chatReport: OnyxEntry; /** Optional details about people involved in the transaction */ personalDetails?: PersonalDetailsList; diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 0468eebb4ad2..d05ec69e531d 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -593,6 +593,7 @@ function ReportActionItem({ ; + /** The chat report associated with the report for this action */ + chatReport: OnyxEntry; + /** ID of the original report from which the given reportAction is first created */ originalReportID?: string; @@ -124,6 +127,7 @@ type ActionContentRouterProps = { function ActionContentRouter({ action, report, + chatReport, originalReportID, iouReport, reportID, @@ -168,6 +172,7 @@ function ActionContentRouter({ action={action} reportID={reportID} chatReportID={report?.type === CONST.REPORT.TYPE.CHAT ? reportID : report?.chatReportID} + chatReport={report?.type === CONST.REPORT.TYPE.CHAT ? report : chatReport} iouReport={report?.type === CONST.REPORT.TYPE.CHAT ? iouReport : report} shouldShowSplitPreview={shouldShowSplitPreview} transactionID={shouldShowSplitPreview ? moneyRequestOriginalMessage?.IOUTransactionID : undefined} @@ -192,6 +197,7 @@ function ActionContentRouter({ iouReportID={getIOUReportIDFromReportActionPreview(action)} policyID={policyID} chatReportID={reportID} + chatReport={report} action={action} isHovered={hovered} isWhisper={isWhisper} diff --git a/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx b/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx index 27a0ae64f4b6..c3f005a74012 100644 --- a/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx +++ b/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx @@ -26,6 +26,9 @@ type ChatTransactionPreviewProps = { /** The ID of the chat report that owns the transaction. Defaults to reportID for chats; expense reports pass their parent chat. */ chatReportID?: string; + /** The chat report that owns the transaction */ + chatReport: OnyxEntry; + /** The IOU report linked to this transaction, used when creating a transaction thread on demand */ iouReport: OnyxEntry; @@ -36,7 +39,7 @@ type ChatTransactionPreviewProps = { transactionID: string | undefined; }; -function ChatTransactionPreview({action, reportID, chatReportID = reportID, iouReport, shouldShowSplitPreview, transactionID}: ChatTransactionPreviewProps) { +function ChatTransactionPreview({action, reportID, chatReportID = reportID, chatReport, iouReport, shouldShowSplitPreview, transactionID}: ChatTransactionPreviewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -51,6 +54,7 @@ function ChatTransactionPreview({action, reportID, chatReportID = reportID, iouR {}} onPaymentOptionsHide={() => {}} isHovered={isHovered} @@ -130,7 +131,7 @@ const getTransactionDisplayAmountAndHeaderText = (transaction: Transaction) => { const isTransactionMadeWithCard = isManagedCardTransaction(transaction); const cashOrCard = isTransactionMadeWithCard ? TestHelper.translateLocal('iou.card') : TestHelper.translateLocal('iou.cash'); const transactionHeaderText = `${date} ${CONST.DOT_SEPARATOR} ${cashOrCard}`; - const transactionDisplayAmount = convertToDisplayString(transaction.amount, transaction.currency); + const transactionDisplayAmount = convertToDisplayString(-transaction.amount, transaction.currency); return {transactionHeaderText, transactionDisplayAmount}; }; @@ -179,7 +180,7 @@ const setReportPreviewData = ( }; const setHasOnceLoadedReportActions = async (hasOnceLoadedReportActions: boolean) => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockChatReport.chatReportID}`, { + await Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockChatReport.reportID}`, { hasOnceLoadedReportActions, }); }; @@ -223,7 +224,7 @@ describe('MoneyRequestReportPreview', () => { for (const transaction of arrayOfTransactions) { const {transactionDisplayAmount, transactionHeaderText} = getTransactionDisplayAmountAndHeaderText(transaction); - expect(screen.getByText(transactionDisplayAmount)).toBeOnTheScreen(); + expect(screen.getAllByText(transactionDisplayAmount).length).toBeGreaterThan(0); expect(screen.getAllByText(transactionHeaderText)).toHaveLength(arrayOfTransactions.length); expect(screen.getAllByText(transaction.merchant)).toHaveLength(arrayOfTransactions.length); } From a3c1d257f72f0928a70ff4d30dcd5f3154ab7dfd Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 18 Jun 2026 18:45:33 +0200 Subject: [PATCH 2/5] fix:pass full chat report to pay actions --- .../MoneyRequestReportPreview/PayActionButton.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx index 4004b46cf5c5..3b31031f06af 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx @@ -19,6 +19,7 @@ import useReportTransactionsCollection from '@hooks/useReportTransactionsCollect import {generateDefaultWorkspaceName} from '@libs/actions/Policy/Policy'; import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import { + getReportOrDraftReport, hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils, hasUpdatedTotal, hasViolations as hasViolationsReportUtils, @@ -171,11 +172,12 @@ function PayActionButton({ } else if (hasHeldExpensesReportUtils(transactions)) { onHoldMenuOpen(CONST.IOU.REPORT_ACTION_TYPE.PAY, type, shouldShowPayButton); } else if (chatReport && iouReport) { + const currentChatReport = getReportOrDraftReport(chatReportID) ?? chatReport; if (isInvoiceReportUtils(iouReport)) { startAnimation(); payInvoice({ paymentMethodType: type, - chatReport, + chatReport: currentChatReport, invoiceReport: iouReport, invoiceReportCurrentNextStepDeprecated: iouReportNextStep, introSelected, @@ -194,7 +196,7 @@ function PayActionButton({ } else { payMoneyRequest({ paymentType: type, - chatReport, + chatReport: currentChatReport, iouReport, introSelected, iouReportCurrentNextStepDeprecated: iouReportNextStep, From 03bab5b113d394c24bc3707be34062130df152f3 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 19 Jun 2026 08:57:50 +0200 Subject: [PATCH 3/5] simplify prop comments --- .../ReportActionItem/MoneyRequestReportPreview/types.ts | 5 +---- src/components/ReportActionItem/TransactionPreview/types.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts b/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts index c7d445dc13ed..95eee088dd10 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts @@ -33,10 +33,7 @@ type MoneyRequestReportPreviewProps = { /** The associated chatReport */ chatReportID: string | undefined; - /** - * The chat report this preview belongs to, threaded down as a heartbeat-stripped stable projection. - * Passed in (instead of subscribed to in-row) so the preview subtree does not re-render on chat heartbeat writes. - */ + /** The chat report this preview belongs to */ chatReport: OnyxEntry; /** The active IOUReport, used for Onyx subscription */ diff --git a/src/components/ReportActionItem/TransactionPreview/types.ts b/src/components/ReportActionItem/TransactionPreview/types.ts index b98d056ba76c..9dc7e577edea 100644 --- a/src/components/ReportActionItem/TransactionPreview/types.ts +++ b/src/components/ReportActionItem/TransactionPreview/types.ts @@ -16,7 +16,7 @@ type TransactionPreviewProps = { /** The associated chatReport */ chatReportID: string | undefined; - /** The associated chat report, passed from the parent to avoid subscribing to chat heartbeat fields in each preview */ + /** The associated chat report */ chatReport: OnyxEntry; /** The ID of the current report */ From 0b39feab73159b2343338e662f376778eaa662a6 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 19 Jun 2026 19:59:59 +0200 Subject: [PATCH 4/5] fix: use full report for hold-menu pay/approve rollback --- src/hooks/useHoldMenuSubmit.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/hooks/useHoldMenuSubmit.ts b/src/hooks/useHoldMenuSubmit.ts index f5082038cfdd..667a35e048cf 100644 --- a/src/hooks/useHoldMenuSubmit.ts +++ b/src/hooks/useHoldMenuSubmit.ts @@ -2,7 +2,7 @@ import {delegateEmailSelector} from '@selectors/Account'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import type {OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; +import {getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {payMoneyRequest} from '@userActions/IOU/PayMoneyRequest'; import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow'; import CONST from '@src/CONST'; @@ -59,9 +59,15 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment const animationCallback = () => onConfirm?.(full); + // `moneyRequestReport`/`chatReport` may be heartbeat-stripped projections (e.g. from the report list). + // The pay/approve actions build their failure rollback by merging the whole report back, so read the full + // reports here to restore the chat's last-message fields if the request fails. + const currentMoneyRequestReport = getReportOrDraftReport(moneyRequestReport?.reportID) ?? moneyRequestReport; + const currentChatReport = getReportOrDraftReport(chatReport?.reportID) ?? chatReport; + if (isApprove) { approveMoneyRequest({ - expenseReport: moneyRequestReport, + expenseReport: currentMoneyRequestReport, policy: activePolicy, currentUserAccountIDParam: currentUserDetails.accountID, currentUserEmailParam: currentUserDetails.email ?? '', @@ -77,11 +83,11 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment expenseReportPolicy: policy, delegateEmail, }); - } else if (chatReport && paymentType) { + } else if (currentChatReport && paymentType) { payMoneyRequest({ paymentType, - chatReport, - iouReport: moneyRequestReport, + chatReport: currentChatReport, + iouReport: currentMoneyRequestReport, introSelected, iouReportCurrentNextStepDeprecated: moneyRequestReportNextStep, currentUserAccountID: currentUserDetails.accountID, From 3d631f40c08d944d837c87c0a44c993865a966e1 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 24 Jun 2026 12:17:45 +0200 Subject: [PATCH 5/5] remove redundant chatReportID prop and fix comment --- .../ReportActionItem/MoneyRequestReportPreview/index.tsx | 1 - .../ReportActionItem/TransactionPreview/index.tsx | 3 ++- .../ReportActionItem/TransactionPreview/types.ts | 3 --- src/hooks/useHoldMenuSubmit.ts | 7 ++++--- .../inbox/report/actionContents/ActionContentRouter.tsx | 1 - .../inbox/report/actionContents/ChatTransactionPreview.tsx | 6 +----- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx index 8be10747bae1..e79b5f41a3db 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx @@ -128,7 +128,6 @@ function MoneyRequestReportPreview({ const renderItem: ListRenderItem = ({item}) => ( ; diff --git a/src/hooks/useHoldMenuSubmit.ts b/src/hooks/useHoldMenuSubmit.ts index 667a35e048cf..94ef8e3f996d 100644 --- a/src/hooks/useHoldMenuSubmit.ts +++ b/src/hooks/useHoldMenuSubmit.ts @@ -59,9 +59,10 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment const animationCallback = () => onConfirm?.(full); - // `moneyRequestReport`/`chatReport` may be heartbeat-stripped projections (e.g. from the report list). - // The pay/approve actions build their failure rollback by merging the whole report back, so read the full - // reports here to restore the chat's last-message fields if the request fails. + // moneyRequestReport/chatReport can be lightweight versions of the report (the report list drops fields + // like the last message text/time so it doesn't re-render on every new message). The pay/approve actions + // restore the report on failure by merging it back in, so we grab the full reports here to make sure the + // chat's last message comes back correctly if the payment fails. const currentMoneyRequestReport = getReportOrDraftReport(moneyRequestReport?.reportID) ?? moneyRequestReport; const currentChatReport = getReportOrDraftReport(chatReport?.reportID) ?? chatReport; diff --git a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx index 964b31f64add..8a77905fe335 100644 --- a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx +++ b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx @@ -171,7 +171,6 @@ function ActionContentRouter({ ; @@ -39,7 +36,7 @@ type ChatTransactionPreviewProps = { transactionID: string | undefined; }; -function ChatTransactionPreview({action, reportID, chatReportID = reportID, chatReport, iouReport, shouldShowSplitPreview, transactionID}: ChatTransactionPreviewProps) { +function ChatTransactionPreview({action, reportID, chatReport, iouReport, shouldShowSplitPreview, transactionID}: ChatTransactionPreviewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -53,7 +50,6 @@ function ChatTransactionPreview({action, reportID, chatReportID = reportID, chat