From 3a3588efe494356285865517d71e037d70d8d6d8 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 9 Jun 2026 17:46:01 +0700 Subject: [PATCH 1/8] Add usePayChatReportActions hook and integrate it into payment components --- .../MoneyReportHeaderSecondaryActions.tsx | 4 +++ .../MoneyReportHeaderSelectionDropdown.tsx | 4 +++ .../PayPrimaryAction.tsx | 4 +++ .../PayActionButton.tsx | 4 +++ src/hooks/useHoldMenuSubmit.ts | 3 ++ src/hooks/useLifecycleActions.tsx | 5 ++-- src/hooks/usePayChatReportActions.ts | 30 +++++++++++++++++++ src/hooks/useSelectionModeReportActions.ts | 4 +++ src/libs/actions/IOU/PayMoneyRequest.ts | 13 ++++++-- tests/actions/IOUTest/PayMoneyRequestTest.ts | 25 ++++++++++++---- 10 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 src/hooks/usePayChatReportActions.ts diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index a40dfdd7d9c8..36c2f8eb34e5 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -31,6 +31,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePaymentOptions from '@hooks/usePaymentOptions'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; @@ -141,6 +142,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); const isAnyTransactionOnHold = hasHeldExpensesReportUtils(allTransactions); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const confirmPayment = ({paymentType: type, payAsBusiness, methodID, paymentMethod}: PaymentActionParams) => { if (!type || !chatReport) { @@ -180,6 +182,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email ?? '', lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { startAnimation(); @@ -203,6 +206,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo onPaid: () => { startAnimation(); }, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx index c786227ce0b4..554f65f00f41 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx @@ -31,6 +31,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePaymentOptions from '@hooks/usePaymentOptions'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; @@ -125,6 +126,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn const activePolicy = usePolicy(activePolicyID); const chatReportPolicy = usePolicy(chatReport?.policyID); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); const {transactionThreadReportID, reportActions} = useTransactionThreadReport(reportID); @@ -283,6 +285,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email ?? '', lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { payMoneyRequest({ @@ -305,6 +308,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn onPaid: () => { startAnimation(); }, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx index bb7bc78ba6a2..c231d8e40c4a 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx @@ -13,6 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePolicy from '@hooks/usePolicy'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; @@ -64,6 +65,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const {convertToDisplayString} = useCurrencyListActions(); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); @@ -146,6 +148,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email ?? '', lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { startAnimation(); @@ -167,6 +170,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, onPaid: startAnimation, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx index 2fd9e05b0ccf..6abf481dc080 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; @@ -99,6 +100,7 @@ function PayActionButton({ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const canAllowSettlement = hasUpdatedTotal(iouReport, policy); const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations, currentUserAccountID, currentUserEmail); @@ -188,6 +190,7 @@ function PayActionButton({ betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { payMoneyRequest({ @@ -207,6 +210,7 @@ function PayActionButton({ amountOwed, ownerBillingGracePeriodEnd, onPaid: startAnimation, + chatReportActions: getChatReportActions(false), }); } } diff --git a/src/hooks/useHoldMenuSubmit.ts b/src/hooks/useHoldMenuSubmit.ts index f5082038cfdd..679dbf19fcb2 100644 --- a/src/hooks/useHoldMenuSubmit.ts +++ b/src/hooks/useHoldMenuSubmit.ts @@ -12,6 +12,7 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; import useOnyx from './useOnyx'; +import usePayChatReportActions from './usePayChatReportActions'; import usePermissions from './usePermissions'; import usePolicy from './usePolicy'; @@ -35,6 +36,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment const activePolicy = usePolicy(activePolicyID); const policy = usePolicy(moneyRequestReport?.policyID); const chatReportPolicy = usePolicy(chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, undefined); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); @@ -97,6 +99,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment ownerBillingGracePeriodEnd, methodID, onPaid: animationCallback, + chatReportActions: getChatReportActions(false), }); } onClose(); diff --git a/src/hooks/useLifecycleActions.tsx b/src/hooks/useLifecycleActions.tsx index d53d292ee079..c4ae37593695 100644 --- a/src/hooks/useLifecycleActions.tsx +++ b/src/hooks/useLifecycleActions.tsx @@ -64,6 +64,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, 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 [chatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`); const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -292,7 +293,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, CONST.IOU.REPORT_ACTION_TYPE.PAY, () => { startAnimation(); - markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? ''); + markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? '', chatReportActions); }, CONST.IOU.PAYMENT_TYPE.ELSEWHERE, ); @@ -300,7 +301,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, } startAnimation(); - markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? ''); + markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? '', chatReportActions); }, }, [CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE]: { diff --git a/src/hooks/usePayChatReportActions.ts b/src/hooks/usePayChatReportActions.ts new file mode 100644 index 000000000000..3a66ce6d6b31 --- /dev/null +++ b/src/hooks/usePayChatReportActions.ts @@ -0,0 +1,30 @@ +import {useCallback} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import {isIndividualInvoiceRoom} from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportActions} from '@src/types/onyx'; +import useOnyx from './useOnyx'; + +/** + * Returns a resolver for the chat report's actions that getPayMoneyRequestParams will use internally. + * + * getPayMoneyRequestParams resolves the chat report from `initialChatReport` but swaps to + * `existingB2BInvoiceReport` when paying an individual invoice room as a business. `payAsBusiness` + * is chosen at click time, so the hook subscribes to both candidates and exposes a callback that + * picks the right slice once the user has chosen. + */ +function usePayChatReportActions(initialChatReport: OnyxEntry, existingB2BInvoiceReport: OnyxEntry): (payAsBusiness: boolean | undefined) => OnyxEntry { + const [initialChatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(initialChatReport?.reportID)}`); + const [b2bInvoiceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(existingB2BInvoiceReport?.reportID)}`); + + return useCallback( + (payAsBusiness: boolean | undefined) => { + const shouldUseB2BInvoiceReport = !!payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(initialChatReport); + return shouldUseB2BInvoiceReport ? b2bInvoiceReportActions : initialChatReportActions; + }, + [initialChatReport, existingB2BInvoiceReport, initialChatReportActions, b2bInvoiceReportActions], + ); +} + +export default usePayChatReportActions; diff --git a/src/hooks/useSelectionModeReportActions.ts b/src/hooks/useSelectionModeReportActions.ts index bb4dea802e5b..3c40cf98b6e4 100644 --- a/src/hooks/useSelectionModeReportActions.ts +++ b/src/hooks/useSelectionModeReportActions.ts @@ -58,6 +58,7 @@ import useLocalize from './useLocalize'; import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; import useParticipantsInvoiceReport from './useParticipantsInvoiceReport'; +import usePayChatReportActions from './usePayChatReportActions'; import usePaymentOptions from './usePaymentOptions'; import usePermissions from './usePermissions'; import usePolicy from './usePolicy'; @@ -127,6 +128,7 @@ function useSelectionModeReportActions({ `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`, ); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const activeAdminPolicies = useActiveAdminPolicies(); const lastWorkspaceNumber = useLastWorkspaceNumber(); const {convertToDisplayString} = useCurrencyListActions(); @@ -388,6 +390,7 @@ function useSelectionModeReportActions({ betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email, lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); clearSelectedTransactions(true); turnOffMobileSelectionMode(); @@ -409,6 +412,7 @@ function useSelectionModeReportActions({ amountOwed, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/libs/actions/IOU/PayMoneyRequest.ts b/src/libs/actions/IOU/PayMoneyRequest.ts index 91e1285c0023..500590c7f0a4 100644 --- a/src/libs/actions/IOU/PayMoneyRequest.ts +++ b/src/libs/actions/IOU/PayMoneyRequest.ts @@ -59,6 +59,7 @@ type PayInvoiceArgs = { betas: OnyxEntry; isSelfTourViewed: boolean | undefined; defaultWorkspaceName: string; + chatReportActions: OnyxEntry; }; type PayMoneyRequestData = { @@ -97,6 +98,7 @@ type PayMoneyRequestFunctionParams = { onPaid?: () => void; // TODO: delegateAccountID will be made required in PR 12 when all callers pass the value (https://github.com/Expensify/App/issues/66425) delegateAccountID?: number | undefined; + chatReportActions: OnyxEntry; }; function getPayMoneyRequestParams({ @@ -121,6 +123,7 @@ function getPayMoneyRequestParams({ defaultWorkspaceName, currentUserLocalCurrency, delegateAccountID, + chatReportActions, }: { initialChatReport: OnyxTypes.Report; iouReport: OnyxEntry; @@ -144,6 +147,7 @@ function getPayMoneyRequestParams({ currentUserLocalCurrency: string | undefined; // TODO: delegateAccountID will be made required in PR 12 when all callers pass the value (https://github.com/Expensify/App/issues/66425) delegateAccountID?: number | undefined; + chatReportActions: OnyxEntry; }): PayMoneyRequestData { // TODO: https://github.com/Expensify/App/issues/66512 // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -239,7 +243,7 @@ function getPayMoneyRequestParams({ // In some instances, the report preview action might not be available to the payer (only whispered to the requestor) // hence we need to make the updates to the action safely. let optimisticReportPreviewAction = null; - const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport?.reportID); + const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport?.reportID, chatReportActions); if (reportPreviewAction) { optimisticReportPreviewAction = updateReportPreview(iouReport, reportPreviewAction, true); } @@ -765,6 +769,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { methodID, onPaid, delegateAccountID, + chatReportActions, } = params; const policyForBillingRestriction = chatReportPolicy ?? (policy?.id === chatReport.policyID ? policy : undefined); if ( @@ -798,6 +803,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { isSelfTourViewed, bankAccountID: paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, delegateAccountID, + chatReportActions, }); // For now, we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with @@ -817,6 +823,7 @@ function markReportPaymentReceived( iouReportCurrentNextStepDeprecated: OnyxEntry, currentUserAccountID: number, currentUserEmail: string, + chatReportActions: OnyxEntry, ) { if (!chatReport || !iouReport) { return; @@ -838,7 +845,7 @@ function markReportPaymentReceived( isSettlingUp: true, }); - const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID); + const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID, chatReportActions); const optimisticReportPreviewAction = reportPreviewAction ? updateReportPreview(iouReport, reportPreviewAction, true) : null; const optimisticNextStepDeprecated = // buildOptimisticNextStep is used in parallel @@ -998,6 +1005,7 @@ function payInvoice({ betas, isSelfTourViewed, defaultWorkspaceName, + chatReportActions, }: PayInvoiceArgs) { const recipient = {accountID: invoiceReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID}; const { @@ -1032,6 +1040,7 @@ function payInvoice({ betas, isSelfTourViewed, defaultWorkspaceName, + chatReportActions, }); const paymentSelected = paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA ? CONST.IOU.PAYMENT_SELECTED.BBA : CONST.IOU.PAYMENT_SELECTED.PBA; diff --git a/tests/actions/IOUTest/PayMoneyRequestTest.ts b/tests/actions/IOUTest/PayMoneyRequestTest.ts index 4ffa711dec26..f157b09b82af 100644 --- a/tests/actions/IOUTest/PayMoneyRequestTest.ts +++ b/tests/actions/IOUTest/PayMoneyRequestTest.ts @@ -253,6 +253,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -465,6 +466,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -631,6 +633,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -685,6 +688,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -802,6 +806,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(partialPayChatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -899,6 +904,7 @@ describe('actions/IOU/PayMoneyRequest', () => { isSelfTourViewed: false, userBillingGracePeriodEnds: undefined, amountOwed: 0, + chatReportActions: undefined, }); await waitForBatchedUpdates(); const newExpenseReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${newExpenseReportID}`); @@ -936,6 +942,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyTrueTour, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -986,6 +993,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFalseTour, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1069,12 +1077,13 @@ describe('actions/IOU/PayMoneyRequest', () => { reportID: outstandingReport.reportID, }; - await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { + const mockChatReportActions = { [reportPreview1.reportActionID]: reportPreview1, [reportPreview2.reportActionID]: reportPreview2, - }); + }; + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, mockChatReportActions); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reimbursedReport.reportID}`, reimbursedReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${outstandingReport.reportID}`, outstandingReport); @@ -1082,7 +1091,7 @@ describe('actions/IOU/PayMoneyRequest', () => { mockFetch?.pause?.(); - markReportPaymentReceived(chatReport, reimbursedReport, undefined, currentUserAccountID, currentUserEmail); + markReportPaymentReceived(chatReport, reimbursedReport, undefined, currentUserAccountID, currentUserEmail, mockChatReportActions); await waitForBatchedUpdates(); const updatedChatReport = await new Promise>((resolve) => { @@ -1151,6 +1160,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: pastDate, policy, chatReportPolicy: policy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1217,6 +1227,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: pastDate, policy: expensePolicy, chatReportPolicy: workspacePolicy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1273,6 +1284,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: futureGraceEnd, chatReportPolicy: workspacePolicy, policy: workspacePolicy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1309,6 +1321,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyAmountZero, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1433,6 +1446,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -1805,6 +1819,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); } await waitForBatchedUpdates(); From 097fe34c2d233aa524fca9122f610c9e55cb7014 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 9 Jun 2026 18:00:34 +0700 Subject: [PATCH 2/8] Refactor usePayChatReportActions hook to simplify return logic and add unit tests for various scenarios --- src/hooks/usePayChatReportActions.ts | 12 +- .../hooks/usePayChatReportActions.test.ts | 132 ++++++++++++++++++ 2 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 tests/unit/hooks/usePayChatReportActions.test.ts diff --git a/src/hooks/usePayChatReportActions.ts b/src/hooks/usePayChatReportActions.ts index 3a66ce6d6b31..2b5aa7053d04 100644 --- a/src/hooks/usePayChatReportActions.ts +++ b/src/hooks/usePayChatReportActions.ts @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isIndividualInvoiceRoom} from '@libs/ReportUtils'; @@ -18,13 +17,10 @@ function usePayChatReportActions(initialChatReport: OnyxEntry, existingB const [initialChatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(initialChatReport?.reportID)}`); const [b2bInvoiceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(existingB2BInvoiceReport?.reportID)}`); - return useCallback( - (payAsBusiness: boolean | undefined) => { - const shouldUseB2BInvoiceReport = !!payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(initialChatReport); - return shouldUseB2BInvoiceReport ? b2bInvoiceReportActions : initialChatReportActions; - }, - [initialChatReport, existingB2BInvoiceReport, initialChatReportActions, b2bInvoiceReportActions], - ); + return (payAsBusiness: boolean | undefined) => { + const shouldUseB2BInvoiceReport = !!payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(initialChatReport); + return shouldUseB2BInvoiceReport ? b2bInvoiceReportActions : initialChatReportActions; + }; } export default usePayChatReportActions; diff --git a/tests/unit/hooks/usePayChatReportActions.test.ts b/tests/unit/hooks/usePayChatReportActions.test.ts new file mode 100644 index 000000000000..489a50cea107 --- /dev/null +++ b/tests/unit/hooks/usePayChatReportActions.test.ts @@ -0,0 +1,132 @@ +import {renderHook, waitFor} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction} from '@src/types/onyx'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const INITIAL_CHAT_REPORT_ID = '1'; +const B2B_INVOICE_REPORT_ID = '2'; + +function buildReport(reportID: string, overrides: Partial = {}): Report { + return { + reportID, + chatType: undefined, + ...overrides, + } as Report; +} + +function buildIndividualInvoiceRoom(reportID: string): Report { + return buildReport(reportID, { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + invoiceReceiver: { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, + accountID: 99, + }, + }); +} + +function buildReportAction(id: string): ReportAction { + return {reportActionID: id} as ReportAction; +} + +const initialChatReportActions = {a1: buildReportAction('a1')}; +const b2bInvoiceReportActions = {b1: buildReportAction('b1')}; + +async function seedReportActions() { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${INITIAL_CHAT_REPORT_ID}`, initialChatReportActions); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${B2B_INVOICE_REPORT_ID}`, b2bInvoiceReportActions); +} + +describe('usePayChatReportActions', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('returns initial chat report actions when payAsBusiness is false', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(false)).toEqual(initialChatReportActions); + }); + }); + + it('returns initial chat report actions when payAsBusiness is undefined', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(undefined)).toEqual(initialChatReportActions); + }); + }); + + it('returns b2b invoice report actions when paying as business from an individual invoice room', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(true)).toEqual(b2bInvoiceReportActions); + }); + }); + + it('falls back to initial chat report actions when payAsBusiness is true but no b2b invoice report exists', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, undefined)); + + await waitFor(() => { + expect(result.current(true)).toEqual(initialChatReportActions); + }); + }); + + it('falls back to initial chat report actions when chat is not an individual invoice room', async () => { + await seedReportActions(); + const initialChatReport = buildReport(INITIAL_CHAT_REPORT_ID, {chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM}); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(true)).toEqual(initialChatReportActions); + }); + }); + + it('falls back to initial chat report actions for a business invoice room (not individual)', async () => { + await seedReportActions(); + const initialChatReport = buildReport(INITIAL_CHAT_REPORT_ID, { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + invoiceReceiver: { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: 'some-policy', + }, + }); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(true)).toEqual(initialChatReportActions); + }); + }); +}); From 475f93ed82499c4b13ccc18394adfd9e3db6edd5 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 12 Jun 2026 23:04:39 +0700 Subject: [PATCH 3/8] Pass chatReportActions in remaining pay callers --- .../Search/SearchList/ListItem/ActionCell/PayActionCell.tsx | 5 +++++ src/hooks/useSearchBulkActions.ts | 2 ++ src/libs/actions/Search.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 094a994f7fdc..fc53ec55d8d9 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -7,6 +7,7 @@ import type {PaymentActionParams} from '@components/SettlementButton/types'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import {useReportPaymentContext} from '@hooks/usePaymentContext'; import usePolicy from '@hooks/usePolicy'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; @@ -64,6 +65,8 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, invoiceReceiverPolicyID, }); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); + const canBePaid = canIOUBePaid(iouReport, chatReport, policy, bankAccountList, currentUserLogin ?? '', currentUserAccountID, transactions, false, undefined, invoiceReceiverPolicy); const shouldOnlyShowElsewhere = !canBePaid && canIOUBePaid(iouReport, chatReport, policy, bankAccountList, currentUserLogin ?? '', currentUserAccountID, transactions, true, undefined, invoiceReceiverPolicy); @@ -101,6 +104,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, isSelfTourViewed, defaultWorkspaceName, additionalOnyxData, + chatReportActions: getChatReportActions(payAsBusiness), }); return; } @@ -123,6 +127,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, additionalOnyxData, + chatReportActions: getChatReportActions(false), }); }; diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 87b9dd722ee8..e2dcfe59cf09 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1071,6 +1071,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { defaultWorkspaceName, additionalOnyxData, shouldPlaySuccessSound: false, + chatReportActions: undefined, }); paidReportCount += 1; continue; @@ -1097,6 +1098,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { paymentItem.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? (paymentItem.bankAccountID ?? workspaceMethodID ?? reportPolicy?.achAccount?.bankAccountID) : undefined, additionalOnyxData, shouldPlaySuccessSound: false, + chatReportActions: undefined, }); paidReportCount += 1; } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index f5c969ebd0cd..558c306b40ba 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -513,6 +513,7 @@ function getPayActionCallback({ ownerBillingGracePeriodEnd, methodID: lastPolicyPaymentMethod === CONST.IOU.PAYMENT_TYPE.VBBA ? snapshotPolicy?.achAccount?.bankAccountID : undefined, additionalOnyxData: getSearchPayOnyxData(hash, item.reportID, currentSearchKey), + chatReportActions: undefined, }); } From 0782f26cb9a45ea6e7ab0b4b3fa13d2f0dbe9a87 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 13 Jun 2026 00:42:05 +0700 Subject: [PATCH 4/8] Pass chatReportActions --- .../Search/SearchList/ListItem/ExpenseReportListItem.tsx | 3 +++ .../Search/SearchList/ListItem/ReportListItemHeader.tsx | 2 ++ .../SearchList/ListItem/TransactionListItem/index.tsx | 2 ++ src/hooks/useSearchBulkActions.ts | 8 ++++++-- src/libs/actions/Search.ts | 8 +++++++- tests/unit/Search/handleActionButtonPressTest.ts | 3 +++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index 8223449df6b5..76e9c846aa0a 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -105,6 +105,7 @@ function ExpenseReportListItem({ }, [searchData, reportItem.reportID]); const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); + const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); const snapshotChatReport = useMemo(() => { const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID; @@ -233,6 +234,7 @@ function ExpenseReportListItem({ chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData, + chatReportActions, }); }, [ currentSearchHash, @@ -265,6 +267,7 @@ function ExpenseReportListItem({ activePolicy, chatReportPolicy, nextStep, + chatReportActions, ]); const handleSelectionButtonPress = useCallback(() => { diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx index 5596e7c933bc..44a9035bae1a 100644 --- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx @@ -240,6 +240,7 @@ function ReportListItemHeader({ }, [snapshot, snapshotReport?.chatReportID, reportItem.parentReportID]); const [parentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(snapshotReport?.policyID ?? reportItem.policyID)}`); const [parentChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); + const [chatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); const chatReport = parentChatReport ?? snapshotChatReport; const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({ reportID: reportItem.reportID, @@ -280,6 +281,7 @@ function ReportListItemHeader({ chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData: snapshot?.data, + chatReportActions, }); }; return !isLargeScreenWidth ? ( diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index 80b3d5339861..2641d76c4743 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -106,6 +106,7 @@ function TransactionListItem({ ]); const currentUserDetails = useCurrentUserPersonalDetails(); const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID)}`); + const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`); const {amountOwed, currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy} = useReportPaymentContext({ reportID: transactionItem.reportID, chatReportPolicyID: parentChatReport?.policyID, @@ -188,6 +189,7 @@ function TransactionListItem({ chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData: currentSearchResults?.data, + chatReportActions, }); }; diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index e2dcfe59cf09..4b973daf4091 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -53,6 +53,7 @@ import { isCurrentUserSubmitter, isDM, isExpenseReport as isExpenseReportUtil, + isIndividualInvoiceRoom, isInvoiceReport, isIOUReport as isIOUReportUtil, isSelfDM, @@ -1052,6 +1053,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { invoiceReceiverPolicyID ?? chatReport.policyID, ); + const shouldUseB2BInvoiceReport = !!paymentItem.payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(chatReport); + const payChatReportID = shouldUseB2BInvoiceReport ? existingB2BInvoiceReport.reportID : chatReport.reportID; + payInvoice({ paymentMethodType: paymentItem.paymentType as PaymentMethodType, chatReport, @@ -1071,7 +1075,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { defaultWorkspaceName, additionalOnyxData, shouldPlaySuccessSound: false, - chatReportActions: undefined, + chatReportActions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${payChatReportID}`], }); paidReportCount += 1; continue; @@ -1098,7 +1102,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { paymentItem.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? (paymentItem.bankAccountID ?? workspaceMethodID ?? reportPolicy?.achAccount?.bankAccountID) : undefined, additionalOnyxData, shouldPlaySuccessSound: false, - chatReportActions: undefined, + chatReportActions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`], }); paidReportCount += 1; } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 558c306b40ba..42e0a3548b1c 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -66,6 +66,7 @@ import type { Policy, Report, ReportAction, + ReportActions, ReportNextStepDeprecated, Transaction, } from '@src/types/onyx'; @@ -198,6 +199,7 @@ type HandleActionButtonPressParams = { chatReportPolicy?: OnyxEntry; iouReportCurrentNextStepDeprecated?: OnyxEntry; searchData?: SearchResultDataType; + chatReportActions: OnyxEntry; }; function handleActionButtonPress({ @@ -228,6 +230,7 @@ function handleActionButtonPress({ chatReportPolicy, iouReportCurrentNextStepDeprecated, searchData, + chatReportActions, }: HandleActionButtonPressParams) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. // We need the transactionID to display the loading indicator for that list item's action. @@ -277,6 +280,7 @@ function handleActionButtonPress({ amountOwed, policy, searchData, + chatReportActions, }); return; case CONST.SEARCH.ACTION_TYPES.APPROVE: @@ -442,6 +446,7 @@ type GetPayActionCallbackParams = { amountOwed: OnyxEntry; policy: OnyxEntry; searchData?: SearchResultDataType; + chatReportActions: OnyxEntry; }; function getPayActionCallback({ @@ -467,6 +472,7 @@ function getPayActionCallback({ amountOwed, policy, searchData, + chatReportActions, }: GetPayActionCallbackParams) { const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, personalPolicyID, lastPaymentMethod, getReportType(item.reportID)); @@ -513,7 +519,7 @@ function getPayActionCallback({ ownerBillingGracePeriodEnd, methodID: lastPolicyPaymentMethod === CONST.IOU.PAYMENT_TYPE.VBBA ? snapshotPolicy?.achAccount?.bankAccountID : undefined, additionalOnyxData: getSearchPayOnyxData(hash, item.reportID, currentSearchKey), - chatReportActions: undefined, + chatReportActions, }); } diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index 810adde86bc1..28553ebed2fb 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -341,6 +341,7 @@ describe('handleActionButtonPress', () => { userBillingGracePeriodEnds: undefined, onHoldMenuOpen: jest.fn(), policy: snapshotPolicy as Policy, + chatReportActions: undefined, }); expect(goToItem).not.toHaveBeenCalled(); }); @@ -360,6 +361,7 @@ describe('handleActionButtonPress', () => { amountOwed: undefined, onHoldMenuOpen, policy: snapshotPolicy as Policy, + chatReportActions: undefined, }); expect(onHoldMenuOpen).toHaveBeenCalledWith(mockReportItemWithHold, CONST.IOU.REPORT_ACTION_TYPE.APPROVE); @@ -379,6 +381,7 @@ describe('handleActionButtonPress', () => { amountOwed: undefined, userBillingGracePeriodEnds: undefined, policy: snapshotPolicy as Policy, + chatReportActions: undefined, }); expect(goToItem).toHaveBeenCalledTimes(0); }); From e7d9e8e3d7c5d3b2492e073128165fe55cd6ed39 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 13 Jun 2026 12:49:05 +0700 Subject: [PATCH 5/8] Add allReportActions to useSearchBulkActions and update test cases --- src/hooks/useSearchBulkActions.ts | 1 + tests/unit/hooks/usePayChatReportActions.test.ts | 11 ++++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 4b973daf4091..8fb459ca8c14 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1126,6 +1126,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { policies, showDelegateNoAccessModal, allReports, + allReportActions, allNextSteps, personalPolicyID, lastPaymentMethods, diff --git a/tests/unit/hooks/usePayChatReportActions.test.ts b/tests/unit/hooks/usePayChatReportActions.test.ts index 489a50cea107..41136ee1876c 100644 --- a/tests/unit/hooks/usePayChatReportActions.test.ts +++ b/tests/unit/hooks/usePayChatReportActions.test.ts @@ -3,7 +3,8 @@ import Onyx from 'react-native-onyx'; import usePayChatReportActions from '@hooks/usePayChatReportActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction} from '@src/types/onyx'; +import type {Report} from '@src/types/onyx'; +import createRandomReportAction from '../../utils/collections/reportActions'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const INITIAL_CHAT_REPORT_ID = '1'; @@ -27,12 +28,8 @@ function buildIndividualInvoiceRoom(reportID: string): Report { }); } -function buildReportAction(id: string): ReportAction { - return {reportActionID: id} as ReportAction; -} - -const initialChatReportActions = {a1: buildReportAction('a1')}; -const b2bInvoiceReportActions = {b1: buildReportAction('b1')}; +const initialChatReportActions = {a1: {...createRandomReportAction(1), reportActionID: 'a1'}}; +const b2bInvoiceReportActions = {b1: {...createRandomReportAction(2), reportActionID: 'b1'}}; async function seedReportActions() { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${INITIAL_CHAT_REPORT_ID}`, initialChatReportActions); From eecf2f47baec4339748ec66de1bf93c477b81b51 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 16 Jun 2026 09:45:41 +0700 Subject: [PATCH 6/8] updated chatReportActions key for Onyx connection --- .../Search/SearchList/ListItem/ExpenseReportListItem.tsx | 2 +- .../Search/SearchList/ListItem/ReportListItemHeader.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index 76e9c846aa0a..88bbea01f505 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -105,7 +105,6 @@ function ExpenseReportListItem({ }, [searchData, reportItem.reportID]); const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); - const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); const snapshotChatReport = useMemo(() => { const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID; @@ -113,6 +112,7 @@ function ExpenseReportListItem({ }, [searchData, snapshotReport?.chatReportID, reportItem.parentReportID]); const chatReport = parentChatReport ?? snapshotChatReport; + const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`); const snapshotPolicy = useMemo(() => { return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy; diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx index 44a9035bae1a..d3593899b3c1 100644 --- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx @@ -240,8 +240,8 @@ function ReportListItemHeader({ }, [snapshot, snapshotReport?.chatReportID, reportItem.parentReportID]); const [parentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(snapshotReport?.policyID ?? reportItem.policyID)}`); const [parentChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); - const [chatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); const chatReport = parentChatReport ?? snapshotChatReport; + const [chatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`); const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({ reportID: reportItem.reportID, chatReportPolicyID: chatReport?.policyID, From f3fee281b26ce9bd9b4632a4a72b91b943debb31 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 16 Jun 2026 09:54:18 +0700 Subject: [PATCH 7/8] beautify changed lines --- .../Search/SearchList/ListItem/ExpenseReportListItem.tsx | 4 +++- .../Search/SearchList/ListItem/ReportListItemHeader.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index 88bbea01f505..c641a1a9f2cd 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -112,7 +112,9 @@ function ExpenseReportListItem({ }, [searchData, snapshotReport?.chatReportID, reportItem.parentReportID]); const chatReport = parentChatReport ?? snapshotChatReport; - const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`); + const [chatReportActions] = originalUseOnyx( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`, + ); const snapshotPolicy = useMemo(() => { return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy; diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx index d3593899b3c1..5837a0461f2a 100644 --- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx @@ -241,7 +241,9 @@ function ReportListItemHeader({ const [parentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(snapshotReport?.policyID ?? reportItem.policyID)}`); const [parentChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); const chatReport = parentChatReport ?? snapshotChatReport; - const [chatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`); + const [chatReportActions] = useOnyx( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`, + ); const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({ reportID: reportItem.reportID, chatReportPolicyID: chatReport?.policyID, From e6ad61e58de273095ca3f871e8e08fdc084fbf4d Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Mon, 22 Jun 2026 14:11:29 +0700 Subject: [PATCH 8/8] Remove unused variable --- src/hooks/useSearchBulkActions.ts | 1 - src/libs/actions/IOU/PayMoneyRequest.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 9bc060b11ea5..fb7ca51f0c6c 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1171,7 +1171,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { lastPaymentMethods, allTransactions, policyIDsWithVBBA, - allReportActions, formatPhoneNumber, clearSelectedTransactions, accountID, diff --git a/src/libs/actions/IOU/PayMoneyRequest.ts b/src/libs/actions/IOU/PayMoneyRequest.ts index 0574138dceee..9b2f8b299f89 100644 --- a/src/libs/actions/IOU/PayMoneyRequest.ts +++ b/src/libs/actions/IOU/PayMoneyRequest.ts @@ -889,7 +889,7 @@ function markReportPaymentReceived( // eslint-disable-next-line @typescript-eslint/no-deprecated const receivedPaymentMessage = getElsewherePaymentReportActionMessage(translateLocal, optimisticIOUReportAction.originalMessage); optimisticIOUReportAction.message = [{html: receivedPaymentMessage, text: receivedPaymentMessage, isEdited: false, type: CONST.REPORT.MESSAGE.TYPE.COMMENT}]; - + const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID, chatReportActions); const optimisticReportPreviewAction = reportPreviewAction ? updateReportPreview(iouReport, reportPreviewAction, true) : null; const optimisticNextStepDeprecated =