From 114d92841c386e4cbd3e4ae9cc3c63e787e97295 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Fri, 10 Jul 2026 20:16:51 +0000 Subject: [PATCH 1/7] Fix: resolve chatReport in PayActionCell so invoices pay from Search transaction rows Co-authored-by: Pujan Shah --- .../ListItem/ActionCell/PayActionCell.tsx | 6 +- tests/ui/components/PayActionCellTest.tsx | 183 ++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 tests/ui/components/PayActionCellTest.tsx diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 461bd7bc4037..40da49390570 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -37,7 +37,7 @@ type PayActionCellProps = { chatReport: OnyxEntry; }; -function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisablePointerEvents, chatReport}: PayActionCellProps) { +function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisablePointerEvents, chatReport: chatReportProp}: PayActionCellProps) { const styles = useThemeStyles(); const {convertToDisplayString} = useCurrencyListActions(); const {isOffline} = useNetwork(); @@ -49,6 +49,10 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisab const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); + // The grouped report rows pass `chatReport` down as a prop, but the flat transaction-row path (e.g. `type:invoice columns:...`) + // renders this cell without it. Fall back to the report's own chat report so the payment flow (guard + payInvoice/payMoneyRequest) + // has the invoice room it needs instead of silently no-opping. + const chatReport = chatReportProp ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(iouReport?.chatReportID)}`]; const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID); const { diff --git a/tests/ui/components/PayActionCellTest.tsx b/tests/ui/components/PayActionCellTest.tsx new file mode 100644 index 000000000000..22b45b51ec4c --- /dev/null +++ b/tests/ui/components/PayActionCellTest.tsx @@ -0,0 +1,183 @@ +import {act, render} from '@testing-library/react-native'; + +import PayActionCell from '@components/Search/SearchList/ListItem/ActionCell/PayActionCell'; +import type {PaymentActionParams} from '@components/SettlementButton/types'; + +import useOnyx from '@hooks/useOnyx'; +import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; + +import {payInvoice} from '@userActions/IOU/PayMoneyRequest'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +import type {UseOnyxResult} from 'react-native-onyx'; + +import React from 'react'; + +const TEST_INVOICE_REPORT_ID = '1001'; +const TEST_CHAT_REPORT_ID = '2002'; +const TEST_HASH = 12345; + +const invoiceReport = { + reportID: TEST_INVOICE_REPORT_ID, + chatReportID: TEST_CHAT_REPORT_ID, + type: CONST.REPORT.TYPE.INVOICE, + currency: CONST.CURRENCY.USD, + policyID: 'policy1', + total: -5000, +} as Report; + +const chatReport = { + reportID: TEST_CHAT_REPORT_ID, + type: CONST.REPORT.TYPE.CHAT, +} as Report; + +function createOnyxResult(value: NonNullable | undefined): UseOnyxResult { + return [value, {status: 'loaded'}]; +} + +// Capture the onPress (confirmPayment) handler PayActionCell passes to the settlement button so the payment can be +// confirmed directly, mirroring a user picking "Pay as an individual > Mark as paid". +const mockOnPressHolder: {current?: (params: PaymentActionParams) => void} = {current: undefined}; +jest.mock('@components/SettlementButton', () => ({ + __esModule: true, + default: (props: {onPress?: (params: PaymentActionParams) => void}) => { + mockOnPressHolder.current = props.onPress; + return null; + }, +})); + +jest.mock('@userActions/IOU/PayMoneyRequest', () => ({ + __esModule: true, + payInvoice: jest.fn(), + payMoneyRequest: jest.fn(), +})); + +jest.mock('@userActions/IOU/ReportWorkflow', () => ({ + __esModule: true, + canIOUBePaid: jest.fn(() => true), +})); + +jest.mock('@libs/actions/Search', () => ({ + __esModule: true, + getSearchPayOnyxData: jest.fn(() => ({optimisticData: [], successData: [], failureData: []})), +})); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + isInvoiceReport: jest.fn(() => true), + isIndividualInvoiceRoom: jest.fn(() => false), + getReimbursableTotal: jest.fn(() => 5000), + }; +}); + +jest.mock('@hooks/useReportWithTransactionsAndViolations', () => ({__esModule: true, default: jest.fn()})); +jest.mock('@hooks/useNetwork', () => ({__esModule: true, default: jest.fn(() => ({isOffline: false}))})); +jest.mock('@hooks/usePolicy', () => ({__esModule: true, default: jest.fn(() => undefined)})); +jest.mock('@hooks/useCurrencyList', () => ({__esModule: true, useCurrencyListActions: jest.fn(() => ({convertToDisplayString: () => '$50.00'}))})); +jest.mock('@hooks/useParticipantsInvoiceReport', () => ({__esModule: true, getParticipantsInvoiceReport: jest.fn(() => undefined)})); +jest.mock('@hooks/usePaymentContext', () => ({ + __esModule: true, + useReportPaymentContext: jest.fn(() => ({ + currentUserLogin: 'payer@test.com', + currentUserAccountID: 1, + email: 'payer@test.com', + localCurrencyCode: 'USD', + introSelected: undefined, + betas: [], + isSelfTourViewed: false, + userBillingGracePeriodEnds: undefined, + amountOwed: undefined, + ownerBillingGracePeriodEnd: undefined, + activePolicyID: undefined, + activePolicy: undefined, + defaultWorkspaceName: '', + nextStep: undefined, + chatReportPolicy: undefined, + })), +})); +jest.mock('@hooks/useOnyx', () => jest.fn()); + +jest.mock('@components/DelegateNoAccessModalProvider', () => ({ + __esModule: true, + useDelegateNoAccessState: jest.fn(() => ({isDelegateAccessRestricted: false})), + useDelegateNoAccessActions: jest.fn(() => ({showDelegateNoAccessModal: jest.fn()})), +})); + +const mockedUseOnyx = jest.mocked(useOnyx); +const mockedUseReportWithTransactionsAndViolations = jest.mocked(useReportWithTransactionsAndViolations); +const mockedPayInvoice = jest.mocked(payInvoice); + +describe('PayActionCell', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockOnPressHolder.current = undefined; + mockedUseReportWithTransactionsAndViolations.mockReturnValue([invoiceReport, [], undefined]); + mockedUseOnyx.mockImplementation((key) => { + if (key === ONYXKEYS.COLLECTION.REPORT) { + return createOnyxResult({[`${ONYXKEYS.COLLECTION.REPORT}${TEST_CHAT_REPORT_ID}`]: chatReport}); + } + return createOnyxResult(undefined); + }); + }); + + it('resolves the chat report from Onyx and calls payInvoice when the chatReport prop is missing (flat transaction row)', () => { + render( + , + ); + + act(() => { + mockOnPressHolder.current?.({ + paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + payAsBusiness: false, + }); + }); + + expect(mockedPayInvoice).toHaveBeenCalledWith( + expect.objectContaining({ + paymentMethodType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + chatReport, + invoiceReport, + }), + ); + }); + + it('does not call payInvoice when the chat report cannot be resolved from the prop or Onyx', () => { + mockedUseOnyx.mockImplementation(() => createOnyxResult(undefined)); + + render( + , + ); + + act(() => { + mockOnPressHolder.current?.({ + paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + payAsBusiness: false, + }); + }); + + expect(mockedPayInvoice).not.toHaveBeenCalled(); + }); +}); From ff59ddfe05d69d20131e82ec21b11148ab28edb6 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Fri, 10 Jul 2026 20:28:34 +0000 Subject: [PATCH 2/7] Fix spellcheck: rephrase 'no-opping' comment Co-authored-by: Pujan Shah --- .../Search/SearchList/ListItem/ActionCell/PayActionCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 40da49390570..106f3555ba2b 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -51,7 +51,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisab const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); // The grouped report rows pass `chatReport` down as a prop, but the flat transaction-row path (e.g. `type:invoice columns:...`) // renders this cell without it. Fall back to the report's own chat report so the payment flow (guard + payInvoice/payMoneyRequest) - // has the invoice room it needs instead of silently no-opping. + // has the invoice room it needs instead of silently doing nothing. const chatReport = chatReportProp ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(iouReport?.chatReportID)}`]; const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID); From 279ec46a2aee4cfa15a16cd3abd922c8b4eaa605 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Sun, 12 Jul 2026 15:36:57 +0000 Subject: [PATCH 3/7] Resolve chatReport in the transaction row and pass it to PayActionCell as a prop Mirror the grouped-report path: resolve the invoice/chat room in TransactionListItemWide and thread it through TransactionItemRow to the Pay action, instead of falling back inside PayActionCell. Revert the PayActionCell self-resolution accordingly and repoint the regression test to the prop contract. Co-authored-by: Pujan Shah --- .../ListItem/ActionCell/PayActionCell.tsx | 6 +----- .../TransactionListItemWide.tsx | 9 +++++++++ .../TransactionItemRowWide.tsx | 2 ++ src/components/TransactionItemRow/types.ts | 2 ++ tests/ui/components/PayActionCellTest.tsx | 17 ++++------------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 106f3555ba2b..461bd7bc4037 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -37,7 +37,7 @@ type PayActionCellProps = { chatReport: OnyxEntry; }; -function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisablePointerEvents, chatReport: chatReportProp}: PayActionCellProps) { +function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisablePointerEvents, chatReport}: PayActionCellProps) { const styles = useThemeStyles(); const {convertToDisplayString} = useCurrencyListActions(); const {isOffline} = useNetwork(); @@ -49,10 +49,6 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisab const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); - // The grouped report rows pass `chatReport` down as a prop, but the flat transaction-row path (e.g. `type:invoice columns:...`) - // renders this cell without it. Fall back to the report's own chat report so the payment flow (guard + payInvoice/payMoneyRequest) - // has the invoice room it needs instead of silently doing nothing. - const chatReport = chatReportProp ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(iouReport?.chatReportID)}`]; const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID); const { diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx index 9fc257f3df30..674d318f0b9e 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx @@ -8,13 +8,17 @@ import TransactionItemRow from '@components/TransactionItemRow'; import {useEditingCellState} from '@components/TransactionItemRow/EditableCell'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useSyncFocus from '@hooks/useSyncFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionInlineEdit from '@hooks/useTransactionInlineEdit'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; + import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {View} from 'react-native'; @@ -58,6 +62,10 @@ function TransactionListItemWide({ const transactionItem = item as unknown as TransactionListItemType; const {isSelected} = useRowSelection(item.keyForList); + // Resolve the transaction's chat report (e.g. the invoice room) and pass it to the Pay action, mirroring the grouped + // report rows in ReportListItemHeader. Without it, Pay from a flat transaction row (e.g. `type:invoice columns:...`) is a no-op. + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionItem.report?.chatReportID ?? transactionItem.report?.parentReportID)}`); + const {isEditingCell, wasRecentlyEditingCell} = useEditingCellState(); const [shouldDisableHoverStyle, setShouldDisableHoverStyle] = useState(false); @@ -178,6 +186,7 @@ function TransactionListItemWide({ diff --git a/src/components/TransactionItemRow/types.ts b/src/components/TransactionItemRow/types.ts index 80f9a8757161..571a34a1f4a0 100644 --- a/src/components/TransactionItemRow/types.ts +++ b/src/components/TransactionItemRow/types.ts @@ -56,6 +56,8 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { type TransactionItemRowProps = { transactionItem: TransactionWithOptionalSearchFields; report?: Report; + /** Chat report the transaction's report belongs to. Passed to the Pay action so flat transaction rows resolve the invoice room the same way grouped report rows do. */ + chatReport?: Report; policy?: Policy; policyCategories?: PolicyCategories; policyTagLists?: PolicyTagLists; diff --git a/tests/ui/components/PayActionCellTest.tsx b/tests/ui/components/PayActionCellTest.tsx index 22b45b51ec4c..fc53da927ba0 100644 --- a/tests/ui/components/PayActionCellTest.tsx +++ b/tests/ui/components/PayActionCellTest.tsx @@ -9,7 +9,6 @@ import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransact import {payInvoice} from '@userActions/IOU/PayMoneyRequest'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; import type {UseOnyxResult} from 'react-native-onyx'; @@ -120,15 +119,10 @@ describe('PayActionCell', () => { jest.clearAllMocks(); mockOnPressHolder.current = undefined; mockedUseReportWithTransactionsAndViolations.mockReturnValue([invoiceReport, [], undefined]); - mockedUseOnyx.mockImplementation((key) => { - if (key === ONYXKEYS.COLLECTION.REPORT) { - return createOnyxResult({[`${ONYXKEYS.COLLECTION.REPORT}${TEST_CHAT_REPORT_ID}`]: chatReport}); - } - return createOnyxResult(undefined); - }); + mockedUseOnyx.mockImplementation(() => createOnyxResult(undefined)); }); - it('resolves the chat report from Onyx and calls payInvoice when the chatReport prop is missing (flat transaction row)', () => { + it('calls payInvoice with the chatReport supplied as a prop (the flat `type:invoice columns:...` transaction row now resolves and passes it)', () => { render( { reportID={TEST_INVOICE_REPORT_ID} hash={TEST_HASH} amount={5000} - // No chatReport prop — this is the flat `type:invoice columns:...` transaction-row path that regressed. - chatReport={undefined} + chatReport={chatReport} />, ); @@ -157,9 +150,7 @@ describe('PayActionCell', () => { ); }); - it('does not call payInvoice when the chat report cannot be resolved from the prop or Onyx', () => { - mockedUseOnyx.mockImplementation(() => createOnyxResult(undefined)); - + it('does not call payInvoice when no chatReport prop is supplied', () => { render( Date: Sun, 12 Jul 2026 18:08:02 +0000 Subject: [PATCH 4/7] Forward chatReport through TransactionItemRow to the Pay action cell TransactionListItemWide already resolves the invoice/chat room and passes it as chatReport to TransactionItemRow, and TransactionItemRowWide already forwards it to DeferredActionCell/PayActionCell. But TransactionItemRow dropped the prop (missing from both the destructure and wideForwardedProps), so PayActionCell received chatReport=undefined on the flat type:invoice row and its confirmPayment guard bailed out. Forwarding the prop completes the chain so payInvoice fires. Co-authored-by: Pujan Shah --- src/components/TransactionItemRow/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 935dbea243bc..034286bf6873 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -37,6 +37,7 @@ const EMPTY_ACTIVE_STYLE: StyleProp = []; function TransactionItemRow({ transactionItem, report, + chatReport, policy, policyCategories, policyTagLists, @@ -182,6 +183,7 @@ function TransactionItemRow({ const wideForwardedProps = { transactionItem, report, + chatReport, policy, policyCategories, policyTagLists, From b7311166408043646e90292cdc5b9a5814b4a992 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Mon, 13 Jul 2026 05:27:38 +0000 Subject: [PATCH 5/7] Reuse the chatReport already derived in TransactionListItem instead of a new Onyx connection TransactionListItemWide added its own useOnyx to resolve the transaction's chat report, but TransactionListItem already derives it as parentChatReport. Pass that one down through sharedProps and drop the redundant subscription in the row. Extend parentChatReport with the parentReportID fallback so it matches the row's prior resolution and the grouped-report path in ReportListItemHeader. Co-authored-by: Pujan Shah --- .../TransactionListItem/TransactionListItemWide.tsx | 9 +-------- .../SearchList/ListItem/TransactionListItem/index.tsx | 3 ++- .../SearchList/ListItem/TransactionListItem/types.ts | 5 ++++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx index 674d318f0b9e..5e6d57660470 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx @@ -8,17 +8,13 @@ import TransactionItemRow from '@components/TransactionItemRow'; import {useEditingCellState} from '@components/TransactionItemRow/EditableCell'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; -import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useSyncFocus from '@hooks/useSyncFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionInlineEdit from '@hooks/useTransactionInlineEdit'; -import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; - import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {View} from 'react-native'; @@ -52,6 +48,7 @@ function TransactionListItemWide({ nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy, currentSearchHash, + chatReport, }: TransactionListItemWideProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -62,10 +59,6 @@ function TransactionListItemWide({ const transactionItem = item as unknown as TransactionListItemType; const {isSelected} = useRowSelection(item.keyForList); - // Resolve the transaction's chat report (e.g. the invoice room) and pass it to the Pay action, mirroring the grouped - // report rows in ReportListItemHeader. Without it, Pay from a flat transaction row (e.g. `type:invoice columns:...`) is a no-op. - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionItem.report?.chatReportID ?? transactionItem.report?.parentReportID)}`); - const {isEditingCell, wasRecentlyEditingCell} = useEditingCellState(); const [shouldDisableHoverStyle, setShouldDisableHoverStyle] = useState(false); diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index 24cd10bc0082..217770a1ea3d 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -138,7 +138,7 @@ function TransactionListItemInner({ transactionItem, ]); const currentUserDetails = useCurrentUserPersonalDetails(); - const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID)}`); + const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`); const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`); const {amountOwed, currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, delegateEmail} = useReportPaymentContext({ reportID: transactionItem.reportID, @@ -257,6 +257,7 @@ function TransactionListItemInner({ policyTagLists, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy, + chatReport: parentChatReport, }; if (!isLargeScreenWidth) { diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts index 89bf86169640..622e0fa743d8 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts @@ -5,7 +5,7 @@ import type {ListItem} from '@components/SelectionList/types'; import type {TransactionPreviewData} from '@libs/actions/Search'; import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab'; -import type {CardList, PolicyCategories, PolicyTagLists, ReportAction, TransactionViolation} from '@src/types/onyx'; +import type {CardList, PolicyCategories, PolicyTagLists, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; type TransactionListItemSharedProps = { item: TItem; @@ -32,6 +32,9 @@ type TransactionListItemSharedProps = { policyTagLists?: PolicyTagLists; nonPersonalAndWorkspaceCards?: CardList; isAttendeesEnabledForMovingPolicy?: boolean; + + /** The chat report (e.g. the invoice room) the transaction's report belongs to, forwarded to the Pay action cell */ + chatReport?: Report; }; type TransactionListItemWideProps = TransactionListItemSharedProps & { From 6b9d7d5e11c9834e77db74381c37d73a472b3f6a Mon Sep 17 00:00:00 2001 From: "{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest/issues/comments#get-an-issue-comment\",\"status\":\"404\"} (via MelvinBot)" Date: Mon, 13 Jul 2026 10:44:58 +0000 Subject: [PATCH 6/7] Remove redundant JSDoc comments on chatReport prop per review Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com> --- .../Search/SearchList/ListItem/TransactionListItem/types.ts | 2 -- src/components/TransactionItemRow/types.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts index 622e0fa743d8..e68bd3a40cda 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/types.ts @@ -32,8 +32,6 @@ type TransactionListItemSharedProps = { policyTagLists?: PolicyTagLists; nonPersonalAndWorkspaceCards?: CardList; isAttendeesEnabledForMovingPolicy?: boolean; - - /** The chat report (e.g. the invoice room) the transaction's report belongs to, forwarded to the Pay action cell */ chatReport?: Report; }; diff --git a/src/components/TransactionItemRow/types.ts b/src/components/TransactionItemRow/types.ts index 571a34a1f4a0..f7f601b6e1ce 100644 --- a/src/components/TransactionItemRow/types.ts +++ b/src/components/TransactionItemRow/types.ts @@ -56,7 +56,6 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { type TransactionItemRowProps = { transactionItem: TransactionWithOptionalSearchFields; report?: Report; - /** Chat report the transaction's report belongs to. Passed to the Pay action so flat transaction rows resolve the invoice room the same way grouped report rows do. */ chatReport?: Report; policy?: Policy; policyCategories?: PolicyCategories; From 77080c7f21956d180f50be6af17f5b99c10273cd Mon Sep 17 00:00:00 2001 From: "{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest/issues/comments#get-an-issue-comment\",\"status\":\"404\"} (via MelvinBot)" Date: Sun, 19 Jul 2026 05:52:51 +0000 Subject: [PATCH 7/7] Preserve Search-snapshot fallback for chatReport in TransactionListItem Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com> --- .../ListItem/TransactionListItem/index.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index 3f4555c53ef8..bdfc82e1aa3b 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -139,11 +139,16 @@ function TransactionListItemInner({ transactionItem, ]); const currentUserDetails = useCurrentUserPersonalDetails(); - const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`); - const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`); + const chatReportID = snapshotReport?.chatReportID ?? snapshotReport?.parentReportID; + const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(chatReportID)}`); + // Fall back to the search snapshot when the chat report isn't in live Onyx yet (e.g. offline or not fetched), + // matching the grouped-report path in ReportListItemHeader so the Pay flow can still resolve the chat report. + const snapshotChatReport = chatReportID ? snapshotData?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined; + const chatReport = parentChatReport ?? snapshotChatReport; + const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? chatReportID)}`); const {amountOwed, currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, delegateEmail} = useReportPaymentContext({ reportID: transactionItem.reportID, - chatReportPolicyID: parentChatReport?.policyID, + chatReportPolicyID: chatReport?.policyID, }); const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector}); @@ -226,7 +231,7 @@ function TransactionListItemInner({ betas, isSelfTourViewed, activePolicy, - chatReport: parentChatReport, + chatReport, chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData: currentSearchResults?.data, @@ -260,7 +265,7 @@ function TransactionListItemInner({ policyTagLists, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy, - chatReport: parentChatReport, + chatReport, }; if (!isLargeScreenWidth) {