From 5efc8ed52d3d2ea49e429ffd950986cada42ba6e Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Mon, 9 Mar 2026 08:27:46 +0000 Subject: [PATCH] Fix infinite loading after cross-report expense merge When merging expenses across different reports, the target transaction's reportID was not being optimistically updated to the destination report. This caused the UI to navigate to a report where the transaction data hadn't been moved yet, triggering the skeleton guard in MoneyRequestView indefinitely. Additionally, if the source report was a single-expense report that happened to be the merge destination, it would be optimistically deleted even though the backend would keep it (with the merged expense). This caused the destination report to appear as null in Onyx. Two changes: 1. Pass newTransactionReportID to getUpdateMoneyRequestParams during merge so the target transaction's reportID is optimistically updated to the destination report. 2. Skip optimistic deletion of the source report when it is the merge destination. Co-authored-by: Situ Chandra Shil --- src/libs/actions/MergeTransaction.ts | 55 ++++++++++++++++++---------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/libs/actions/MergeTransaction.ts b/src/libs/actions/MergeTransaction.ts index dbd8a345aa68..9196cb517608 100644 --- a/src/libs/actions/MergeTransaction.ts +++ b/src/libs/actions/MergeTransaction.ts @@ -194,6 +194,7 @@ function getOnyxTargetTransactionData({ currentUserAccountIDParam, currentUserEmailParam, isASAPSubmitBetaEnabled, + destinationReportID, }: { targetTransaction: Transaction; targetTransactionViolations: OnyxEntry; @@ -207,6 +208,7 @@ function getOnyxTargetTransactionData({ currentUserAccountIDParam: number; currentUserEmailParam: string; isASAPSubmitBetaEnabled: boolean; + destinationReportID: string | undefined; }) { let data: UpdateMoneyRequestData; const isUnreportedExpense = !mergeTransaction.reportID || mergeTransaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; @@ -250,6 +252,7 @@ function getOnyxTargetTransactionData({ currentUserAccountIDParam, currentUserEmailParam, isASAPSubmitBetaEnabled, + newTransactionReportID: destinationReportID, }); } @@ -348,6 +351,13 @@ function mergeTransactionRequest({ reportID: mergeTransaction.reportID, }; + // When the destination report differs from the target transaction's current report, + // pass the destination reportID so the target transaction's reportID is optimistically + // updated. Without this, the UI navigates to the destination report but the transaction + // data hasn't been moved there yet, causing infinite loading. + const isUnreportedDestination = !mergeTransaction.reportID || mergeTransaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; + const destinationReportID = !isUnreportedDestination && mergeTransaction.reportID !== targetTransaction.reportID ? mergeTransaction.reportID : undefined; + const onyxTargetTransactionData = getOnyxTargetTransactionData({ targetTransaction, targetTransactionViolations: allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + targetTransaction.transactionID] ?? [], @@ -361,6 +371,7 @@ function mergeTransactionRequest({ currentUserAccountIDParam, currentUserEmailParam, isASAPSubmitBetaEnabled, + destinationReportID, }); // Optimistic delete the source transaction and also delete its report if it was a single expense report @@ -386,28 +397,32 @@ function mergeTransactionRequest({ value: sourceTransaction, }; const transactionsOfSourceReport = getReportTransactions(sourceTransaction.reportID); - const optimisticSourceReportData: Array> = - transactionsOfSourceReport.length === 1 - ? [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${sourceTransaction.reportID}`, - value: null, - }, - ] - : []; + // Don't delete the source report if it's the merge destination, because the + // backend will move the merged transaction there. Deleting it optimistically + // would cause the destination report to appear as null in Onyx, leading to + // infinite loading when navigating to it after the merge. + const isSourceReportTheMergeDestination = sourceTransaction.reportID === mergeTransaction.reportID; + const shouldDeleteSourceReport = transactionsOfSourceReport.length === 1 && !isSourceReportTheMergeDestination; + const optimisticSourceReportData: Array> = shouldDeleteSourceReport + ? [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${sourceTransaction.reportID}`, + value: null, + }, + ] + : []; // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - const failureSourceReportData: Array> = - transactionsOfSourceReport.length === 1 - ? [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${sourceTransaction.reportID}`, - value: getReportOrDraftReport(sourceTransaction.reportID), - }, - ] - : []; + const failureSourceReportData: Array> = shouldDeleteSourceReport + ? [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${sourceTransaction.reportID}`, + value: getReportOrDraftReport(sourceTransaction.reportID), + }, + ] + : []; const iouActionOfSourceTransaction = getIOUActionForReportID(sourceTransaction.reportID, sourceTransaction.transactionID); const optimisticSourceReportActionData: Array> = iouActionOfSourceTransaction ? [