From 72762ce891a52b48f84b34fe7eb7289dff79bd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 22 Jan 2026 14:51:30 -0500 Subject: [PATCH 01/16] fix: migrate ReportActionItem to useOnyx for policyTags This properly migrates the ReportActionItem component to use the useOnyx hook for policyTags data instead of relying on the deprecated module-level Onyx.connect pattern in ModifiedExpenseMessage.ts. Changes: - Import getForReportActionTemp instead of getForReportAction - Add useLocalize hook to get translate function - Add useOnyx hook to fetch policyTags for the reports policy - Pass translate, policy, and policyTags to getForReportActionTemp - Add eslint-disable comments for remaining Onyx.connectWithoutView calls (used by utility files that will be migrated in follow-up PRs) --- src/libs/ModifiedExpenseMessage.ts | 2 ++ src/pages/inbox/report/ReportActionItem.tsx | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index c23c05bbc2b42..cf534f4811576 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -28,6 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat, import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; +// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, @@ -44,6 +45,7 @@ let environmentURL: string; getEnvironmentURL().then((url: string) => (environmentURL = url)); let currentUserLogin = ''; +// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, callback: (value) => { diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index a8dece8f1384a..e37013c4fb1bb 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -7,7 +7,7 @@ import useOnyx from '@hooks/useOnyx'; import useOriginalReportID from '@hooks/useOriginalReportID'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import {getForReportAction, getMovedReportID} from '@libs/ModifiedExpenseMessage'; +import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils'; import { chatIncludesChronosWithID, @@ -100,6 +100,8 @@ function ReportActionItem({ const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; const isOriginalReportArchived = useReportIsArchived(originalReportID); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const {translate} = useLocalize(); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true}); @@ -162,12 +164,13 @@ function ReportActionItem({ action as OnyxEntry>, report, )} - modifiedExpenseMessage={getForReportAction({ + modifiedExpenseMessage={getForReportActionTemp({ + translate, reportAction: action, - policyID: report?.policyID, + policy, movedFromReport, movedToReport, - policyForMovingExpensesID, + policyTags, })} getTransactionsWithReceipts={getTransactionsWithReceipts} clearError={clearError} From d0ff5e234488763c6bc06aad41c04f7da168fc7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 22 Jan 2026 14:55:12 -0500 Subject: [PATCH 02/16] fix: improve eslint-disable comments with more specific justification --- src/libs/ModifiedExpenseMessage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index cf534f4811576..8af66ecfaa2fe 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -28,7 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat, import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs +// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to maintain global policyTags state for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored. Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, @@ -45,7 +45,7 @@ let environmentURL: string; getEnvironmentURL().then((url: string) => (environmentURL = url)); let currentUserLogin = ''; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs +// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to access session data globally for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored. Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, callback: (value) => { From 58e8860562dc628929322eb95104c54a74f26c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 22 Jan 2026 15:37:00 -0500 Subject: [PATCH 03/16] fix: add currentUserLogin parameter to getForReportActionTemp The function was still using module-level currentUserLogin from Onyx.connect. Now it accepts currentUserLogin as a parameter for full migration. Changes: - Add currentUserLogin parameter to getForReportActionTemp - Update ReportActionItem.tsx to pass currentUserLogin from session - Update ContextMenuActions.tsx to pass currentUserLogin - Update BaseReportActionContextMenu.tsx to fetch session and pass currentUserLogin --- Mobile-Expensify | 2 +- src/libs/ModifiedExpenseMessage.ts | 4 +++- src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 1 + src/pages/inbox/report/ReportActionItem.tsx | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index a92777fbadfc6..0d187767eb984 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit a92777fbadfc660e19508d03464e4355324ce4d4 +Subproject commit 0d187767eb984c387a981f45902ff285d67b76ff diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 8af66ecfaa2fe..2e847a291d256 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -516,6 +516,7 @@ function getForReportActionTemp({ movedFromReport, movedToReport, policyTags, + currentUserLogin: currentUserLoginParam, }: { translate: LocalizedTranslate; reportAction: OnyxEntry; @@ -523,6 +524,7 @@ function getForReportActionTemp({ movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; policyTags: OnyxEntry; + currentUserLogin: string; }): string { if (!isModifiedExpenseAction(reportAction)) { return ''; @@ -612,7 +614,7 @@ function getForReportActionTemp({ if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.AI) { categoryLabel += ` ${translate('iou.basedOnAI')}`; } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) { - const isAdmin = isPolicyAdmin(policy, currentUserLogin); + const isAdmin = isPolicyAdmin(policy, currentUserLoginParam); // For admins, create a hyperlink to the workspace rules page if (isAdmin && policy?.id) { diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index c7c4cfa6aeb8c..8fe9824840c49 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -739,6 +739,7 @@ const ContextMenuActions: ContextMenuAction[] = [ movedFromReport, movedToReport, policyTags, + currentUserLogin: currentUserPersonalDetails?.login ?? '', }); Clipboard.setString(modifyExpenseMessage); } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index e37013c4fb1bb..a4eaa79db6cf9 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -102,6 +102,7 @@ function ReportActionItem({ const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {translate} = useLocalize(); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true}); @@ -171,6 +172,7 @@ function ReportActionItem({ movedFromReport, movedToReport, policyTags, + currentUserLogin: session?.email ?? '', })} getTransactionsWithReceipts={getTransactionsWithReceipts} clearError={clearError} From b728cbed65a0dd99d6d4220e1efb02434956147e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 22 Jan 2026 16:22:49 -0500 Subject: [PATCH 04/16] fix: add currentUserLogin to destructuring in copy to clipboard action --- src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index 8fe9824840c49..387bce52223db 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -714,6 +714,7 @@ const ContextMenuActions: ContextMenuAction[] = [ policyTags, translate, harvestReport, + currentUserLogin, }, ) => { const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction); From 0821bd14adcef674e9f6b56e62f1471a80c20bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 22 Jan 2026 17:22:59 -0500 Subject: [PATCH 05/16] fix: remove unused policyForMovingExpensesID --- src/pages/inbox/report/ReportActionItem.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index a4eaa79db6cf9..a47b2ec662845 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -5,7 +5,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useOriginalReportID from '@hooks/useOriginalReportID'; -import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils'; @@ -112,7 +111,6 @@ function ReportActionItem({ const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); - const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID || undefined}`]; From f8f7357f66b5e45d29388b820d5be2e81ef69719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 22 Jan 2026 17:46:52 -0500 Subject: [PATCH 06/16] chore: trigger CI checks From 128592e02c87174dbcba71effc5482759983793e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 3 Feb 2026 20:23:06 -0600 Subject: [PATCH 07/16] fix: address PR review comments - Use useCurrentUserPersonalDetails hook for currentUserLogin instead of session - Use policyForMovingExpensesID when policyID is fake for policy tags - Update ESLint disable comments with more specific justifications --- src/libs/ModifiedExpenseMessage.ts | 4 ++-- src/pages/inbox/report/ReportActionItem.tsx | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 2e847a291d256..90aa5e4dc8a23 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -28,7 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat, import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to maintain global policyTags state for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored. +// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction is called by utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. The global policyTags state is required for these non-React contexts. Migration to useOnyx will happen when these utility files are converted to custom hooks. Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, @@ -45,7 +45,7 @@ let environmentURL: string; getEnvironmentURL().then((url: string) => (environmentURL = url)); let currentUserLogin = ''; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to access session data globally for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored. +// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction requires currentUserLogin for isPolicyAdmin checks. This function is called by non-React utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. Migration to useOnyx will happen when these utility files are converted to custom hooks. Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, callback: (value) => { diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index a47b2ec662845..67bff38f8ab5c 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -5,6 +5,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useOriginalReportID from '@hooks/useOriginalReportID'; +import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils'; @@ -98,10 +99,11 @@ function ReportActionItem({ const originalReportID = useOriginalReportID(reportID, action); const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; const isOriginalReportArchived = useReportIsArchived(originalReportID); - const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const {translate} = useLocalize(); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); + const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails(); + const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); + // Use policyForMovingExpensesID when policyID is fake to ensure tag edits on moved expenses use the correct workspace's tag lists + const policyIDForTags = report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE && policyForMovingExpensesID ? policyForMovingExpensesID : report?.policyID; + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForTags}`, {canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true}); @@ -170,7 +172,7 @@ function ReportActionItem({ movedFromReport, movedToReport, policyTags, - currentUserLogin: session?.email ?? '', + currentUserLogin: currentUserLogin ?? '', })} getTransactionsWithReceipts={getTransactionsWithReceipts} clearError={clearError} From 14255aa6b041b081733ecce84df86c58a567d730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 3 Feb 2026 20:42:17 -0600 Subject: [PATCH 08/16] fix: revert Mobile-Expensify submodule change Reverts accidental submodule pointer change from rebase. --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 0d187767eb984..a92777fbadfc6 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 0d187767eb984c387a981f45902ff285d67b76ff +Subproject commit a92777fbadfc660e19508d03464e4355324ce4d4 From acb30b74700b0cfcd99674b404996754ba469de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 3 Feb 2026 20:54:07 -0600 Subject: [PATCH 09/16] fix: TypeScript error in ContextMenuActions Use currentUserPersonalDetails in destructuring instead of currentUserLogin. --- src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index 387bce52223db..b01f6f39761c2 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -714,7 +714,7 @@ const ContextMenuActions: ContextMenuAction[] = [ policyTags, translate, harvestReport, - currentUserLogin, + currentUserPersonalDetails, }, ) => { const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction); From be64c7f8692f09615d96f301d51bef0149f8f63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 4 Feb 2026 16:00:53 -0600 Subject: [PATCH 10/16] test: add unit tests for getForReportActionTemp Add comprehensive unit tests for getForReportActionTemp function to ensure it behaves the same as getForReportAction. These tests cover: - Amount changes - Description changes - Merchant set/remove - Category changes with AI/MCC attribution - Distance changes - Moving expenses - Edge cases (non-modified-expense actions, empty messages) Once the migration from getForReportAction to getForReportActionTemp is complete, the tests for getForReportAction can be safely removed. --- tests/unit/ModifiedExpenseMessageTest.ts | 301 ++++++++++++++++++++++- 1 file changed, 300 insertions(+), 1 deletion(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 962e2cbe25923..0ed4b258ae19f 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -1,5 +1,5 @@ import {getEnvironmentURL} from '@libs/Environment/Environment'; -import {getForReportAction, getMovedFromOrToReportMessage, getMovedReportID} from '@libs/ModifiedExpenseMessage'; +import {getForReportAction, getForReportActionTemp, getMovedFromOrToReportMessage, getMovedReportID} from '@libs/ModifiedExpenseMessage'; // eslint-disable-next-line no-restricted-syntax -- this is required to allow mocking import * as PolicyUtils from '@libs/PolicyUtils'; // eslint-disable-next-line no-restricted-syntax -- this is required to allow mocking @@ -952,4 +952,303 @@ describe('ModifiedExpenseMessage', () => { }); }); }); + + describe('getForReportActionTemp', () => { + // getForReportActionTemp is a temporary function that takes translate, policy, policyTags, and currentUserLogin as parameters + // instead of using module-level Onyx connections. This allows React components to properly re-render when the underlying data changes. + // These tests mirror the getForReportAction tests to ensure the same behavior. + + describe('when the amount is changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `changed the amount to $18.00 (previously $12.55)`; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the amount is changed and the description is removed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + newComment: '', + oldComment: 'this is for the shuttle', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = 'changed the amount to $18.00 (previously $12.55)\nremoved the description (previously "this is for the shuttle")'; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant is set', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldMerchant: '', + merchant: 'Big Belly', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `set the merchant to "Big Belly"`; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant is removed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + merchant: '', + oldMerchant: 'Big Belly', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `removed the merchant (previously "Big Belly")`; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the category is changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + category: 'Travel', + oldCategory: 'Food', + } as OriginalMessageModifiedExpense, + }; + + it('returns the correct text message', () => { + const expectedResult = `changed the category to "Travel" (previously "Food")`; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the category is changed with AI attribution', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + category: 'Travel', + oldCategory: 'Food', + source: CONST.CATEGORY_SOURCE.AI, + } as OriginalMessageModifiedExpense, + }; + + it('returns the correct text message with AI attribution', () => { + const expectedResult = `changed the category based on past activity to "Travel" (previously "Food")`; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the category is changed with MCC attribution', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + category: 'Travel', + oldCategory: 'Food', + source: CONST.CATEGORY_SOURCE.MCC, + } as OriginalMessageModifiedExpense, + }; + + it('returns the correct text message with MCC attribution for non-admin', () => { + const expectedResult = `changed the category based on workspace rule to "Travel" (previously "Food")`; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + + it('returns the correct workspace rules link for admin', () => { + const mockPolicy: Policy = { + id: 'AbC123XyZ789', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + owner: 'test@example.com', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + }; + + jest.spyOn(PolicyUtils, 'isPolicyAdmin').mockReturnValue(true); + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policy: mockPolicy, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + // Verify the policyID in the URL exactly matches the policy.id (case-preserved) + expect(result).toContain(`workspaces/${mockPolicy.id}/rules`); + expect(result).toContain('href='); + expect(result).toContain('workspace rules'); + }); + }); + + describe('when the distance is changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldMerchant: '1.00 mi @ $0.70 / mi', + merchant: '10.00 mi @ $0.70 / mi', + oldAmount: 70, + amount: 700, + oldCurrency: CONST.CURRENCY.USD, + currency: CONST.CURRENCY.USD, + }, + }; + + it('then the message says the distance is changed and shows the new and old merchant and amount', () => { + const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $7.00 (previously $0.70)`; + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + expect(result).toEqual(expectedResult); + }); + }); + + describe('when moving an expense', () => { + it('returns the movedFromOrToReportMessage message when provided', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + }; + + const expectedResult = 'moved an expense'; + + const movedFromReport = { + ...createRandomReport(1, undefined), + reportName: '', + }; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + movedFromReport, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the report action is not a modified expense action', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + }; + + it('returns an empty string', () => { + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(''); + }); + }); + + describe('when there are no changes in the original message', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: {}, + }; + + it('returns the generic changed expense message', () => { + const expectedResult = 'changed the expense'; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + }); }); From a2dceaff0d6ef6979860cf130d83587f5b79cbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 5 Feb 2026 16:28:56 -0600 Subject: [PATCH 11/16] Address PR review feedback: simplify eslint comments, revert param rename, and revert submodule - Simplify eslint-disable comments to reference issue #66336 instead of verbose explanations - Revert currentUserLogin parameter rename (keep original name instead of currentUserLoginParam) - Revert unintended Mobile-Expensify submodule change --- Mobile-Expensify | 2 +- src/libs/ModifiedExpenseMessage.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 574c396c17314..b9694c7c450b0 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 574c396c1731467091f7865a3e460343d25e2397 +Subproject commit b9694c7c450b09aa59690792b178d36bcab911f7 diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 90aa5e4dc8a23..90783aa2ab24c 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -28,7 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat, import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction is called by utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. The global policyTags state is required for these non-React contexts. Migration to useOnyx will happen when these utility files are converted to custom hooks. +// eslint-disable-next-line @typescript-eslint/no-deprecated -- Onyx.connectWithoutView is being removed in https://github.com/Expensify/App/issues/66336 Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, @@ -45,7 +45,7 @@ let environmentURL: string; getEnvironmentURL().then((url: string) => (environmentURL = url)); let currentUserLogin = ''; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction requires currentUserLogin for isPolicyAdmin checks. This function is called by non-React utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. Migration to useOnyx will happen when these utility files are converted to custom hooks. +// eslint-disable-next-line @typescript-eslint/no-deprecated -- Onyx.connectWithoutView is being removed in https://github.com/Expensify/App/issues/66336 Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, callback: (value) => { @@ -516,7 +516,7 @@ function getForReportActionTemp({ movedFromReport, movedToReport, policyTags, - currentUserLogin: currentUserLoginParam, + currentUserLogin, }: { translate: LocalizedTranslate; reportAction: OnyxEntry; @@ -614,7 +614,7 @@ function getForReportActionTemp({ if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.AI) { categoryLabel += ` ${translate('iou.basedOnAI')}`; } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) { - const isAdmin = isPolicyAdmin(policy, currentUserLoginParam); + const isAdmin = isPolicyAdmin(policy, currentUserLogin); // For admins, create a hyperlink to the workspace rules page if (isAdmin && policy?.id) { From 0bd9a8daede03942973ddeb1e302c6a604b9aebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 5 Feb 2026 20:07:00 -0600 Subject: [PATCH 12/16] fix: rename module-level currentUserLogin to storedCurrentUserLogin to avoid no-shadow lint error Co-authored-by: Cursor --- src/libs/ModifiedExpenseMessage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 42cc5e17f2d94..db4054336fb5c 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -44,7 +44,7 @@ Onyx.connectWithoutView({ let environmentURL: string; getEnvironmentURL().then((url: string) => (environmentURL = url)); -let currentUserLogin = ''; +let storedCurrentUserLogin = ''; // eslint-disable-next-line @typescript-eslint/no-deprecated -- Onyx.connectWithoutView is being removed in https://github.com/Expensify/App/issues/66336 Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, @@ -53,7 +53,7 @@ Onyx.connectWithoutView({ if (!value) { return; } - currentUserLogin = value?.email ?? ''; + storedCurrentUserLogin = value?.email ?? ''; }, }); @@ -166,7 +166,7 @@ function getForExpenseMovedFromSelfDM(translate: LocalizedTranslate, destination // In NewDot, the "Move report" flow only supports moving expenses from self-DM to: // - A policy expense chat // - A 1:1 DM - const currentUserAccountID = getPersonalDetailByEmail(currentUserLogin)?.accountID; + const currentUserAccountID = getPersonalDetailByEmail(storedCurrentUserLogin)?.accountID; const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName({report: rootParentReport}) : buildReportNameFromParticipantNames({report: rootParentReport, currentUserAccountID}); @@ -332,7 +332,7 @@ function getForReportAction({ } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) { // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(policyID); - const isAdmin = isPolicyAdmin(policy, currentUserLogin); + const isAdmin = isPolicyAdmin(policy, storedCurrentUserLogin); // For admins, create a hyperlink to the workspace rules page if (isAdmin && policy?.id) { From cbec8b1a3fbee796f0f6423184a79c759b929e93 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 10 Feb 2026 17:55:38 +0000 Subject: [PATCH 13/16] Fix billable/reimbursable localization and add AI-generated fallback in getForReportActionTemp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate billable/reimbursable values before passing to buildMessageFragmentForValue to match getForReportAction behavior. Add missing AI-generated fallback when message is empty. Add test cases for both fixes. Co-authored-by: Marco Chávez --- src/libs/ModifiedExpenseMessage.ts | 21 ++++++-- tests/unit/ModifiedExpenseMessageTest.ts | 69 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index db4054336fb5c..792f44a3bc72e 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -697,10 +697,12 @@ function getForReportActionTemp({ const hasModifiedBillable = isReportActionOriginalMessageAnObject && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { + const oldBillable = reportActionOriginalMessage?.oldBillable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase(); + const newBillable = reportActionOriginalMessage?.billable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase(); buildMessageFragmentForValue( translate, - reportActionOriginalMessage?.billable ?? '', - reportActionOriginalMessage?.oldBillable ?? '', + newBillable, + oldBillable, translate('iou.expense'), true, setFragments, @@ -711,10 +713,14 @@ function getForReportActionTemp({ const hasModifiedReimbursable = isReportActionOriginalMessageAnObject && 'oldReimbursable' in reportActionOriginalMessage && 'reimbursable' in reportActionOriginalMessage; if (hasModifiedReimbursable) { + const oldReimbursable = + reportActionOriginalMessage?.oldReimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase(); + const newReimbursable = + reportActionOriginalMessage?.reimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase(); buildMessageFragmentForValue( translate, - reportActionOriginalMessage?.reimbursable ?? '', - reportActionOriginalMessage?.oldReimbursable ?? '', + newReimbursable, + oldReimbursable, translate('iou.expense'), true, setFragments, @@ -745,6 +751,13 @@ function getForReportActionTemp({ getMessageLine(translate, `\n${translate('iou.removed')}`, removalFragments); if (message === '') { + // If we don't have enough structured information to build a detailed message but we + // know the change was AI-generated, fall back to an AI-attributed generic summary so + // users can still understand that Concierge updated the expense automatically. + if (reportActionOriginalMessage?.aiGenerated) { + return `${translate('iou.changedTheExpense')} ${translate('iou.basedOnAI')}`; + } + return translate('iou.changedTheExpense'); } return `${message.substring(1, message.length)}`; diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 0ed4b258ae19f..0f7dcf1778693 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -1250,5 +1250,74 @@ describe('ModifiedExpenseMessage', () => { expect(result).toEqual(expectedResult); }); }); + + describe('when the billable field is changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldBillable: 'nonBillable', + billable: 'billable', + }, + }; + + it('returns the correct translated text message', () => { + const expectedResult = 'changed the expense to "billable" (previously "non-billable")'; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the reimbursable field is changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldReimbursable: 'reimbursable', + reimbursable: 'nonReimbursable', + }, + }; + + it('returns the correct translated text message', () => { + const expectedResult = 'changed the expense to "non-reimbursable" (previously "reimbursable")'; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when there are no changes but aiGenerated is true', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: {aiGenerated: true}, + }; + + it('returns the AI-attributed message', () => { + const expectedResult = 'changed the expense based on past activity'; + + const result = getForReportActionTemp({ + translate: translateLocal, + reportAction, + policyTags: undefined, + currentUserLogin: 'test@example.com', + }); + + expect(result).toEqual(expectedResult); + }); + }); }); }); From 0515fdaaf762f2e3254f6b602b3c3a4d19cac9a3 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 10 Feb 2026 18:38:37 +0000 Subject: [PATCH 14/16] Fix Prettier formatting in ModifiedExpenseMessage.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marco Chávez --- src/libs/ModifiedExpenseMessage.ts | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 792f44a3bc72e..3c4775b6d8cb8 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -699,34 +699,15 @@ function getForReportActionTemp({ if (hasModifiedBillable) { const oldBillable = reportActionOriginalMessage?.oldBillable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase(); const newBillable = reportActionOriginalMessage?.billable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase(); - buildMessageFragmentForValue( - translate, - newBillable, - oldBillable, - translate('iou.expense'), - true, - setFragments, - removalFragments, - changeFragments, - ); + buildMessageFragmentForValue(translate, newBillable, oldBillable, translate('iou.expense'), true, setFragments, removalFragments, changeFragments); } const hasModifiedReimbursable = isReportActionOriginalMessageAnObject && 'oldReimbursable' in reportActionOriginalMessage && 'reimbursable' in reportActionOriginalMessage; if (hasModifiedReimbursable) { const oldReimbursable = reportActionOriginalMessage?.oldReimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase(); - const newReimbursable = - reportActionOriginalMessage?.reimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase(); - buildMessageFragmentForValue( - translate, - newReimbursable, - oldReimbursable, - translate('iou.expense'), - true, - setFragments, - removalFragments, - changeFragments, - ); + const newReimbursable = reportActionOriginalMessage?.reimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase(); + buildMessageFragmentForValue(translate, newReimbursable, oldReimbursable, translate('iou.expense'), true, setFragments, removalFragments, changeFragments); } const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'newAttendees' in reportActionOriginalMessage; From 7d18ddbb6e1e0773a4438e06d3e78a58d4db60c3 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 10 Feb 2026 22:49:18 +0000 Subject: [PATCH 15/16] Improve policyID comment and add DEFAULT_TAG_LIST fallback for policyTags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the comment on policyIDForTags to explain why a fake policyID requires falling back to policyForMovingExpensesID. Add CONST.POLICY.DEFAULT_TAG_LIST fallback when policyTags is undefined to prevent tag-only edits from degrading to the generic "changed the expense" message. Co-authored-by: Marco Chávez --- src/pages/inbox/report/ReportActionItem.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 67bff38f8ab5c..3294c777d923e 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -101,7 +101,10 @@ function ReportActionItem({ const isOriginalReportArchived = useReportIsArchived(originalReportID); const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); - // Use policyForMovingExpensesID when policyID is fake to ensure tag edits on moved expenses use the correct workspace's tag lists + // When an expense is moved from a self-DM to a workspace, the report's policyID is temporarily + // set to a fake placeholder (CONST.POLICY.OWNER_EMAIL_FAKE). Looking up POLICY_TAGS with that + // fake ID would return nothing, so we fall back to policyForMovingExpensesID (the actual + // destination workspace) to fetch the correct tag list for display. const policyIDForTags = report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE && policyForMovingExpensesID ? policyForMovingExpensesID : report?.policyID; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForTags}`, {canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); @@ -171,7 +174,7 @@ function ReportActionItem({ policy, movedFromReport, movedToReport, - policyTags, + policyTags: policyTags ?? CONST.POLICY.DEFAULT_TAG_LIST, currentUserLogin: currentUserLogin ?? '', })} getTransactionsWithReceipts={getTransactionsWithReceipts} From 30b188a76e5af62b59be6617b61cbbde3fd21408 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Wed, 11 Feb 2026 02:50:42 +0000 Subject: [PATCH 16/16] Fix: use email instead of login from useCurrentUserPersonalDetails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CurrentUserPersonalDetailsProvider backfills email from session but not login, so login can be undefined during loading. Use email to maintain parity with the old session-backed value. Co-authored-by: Marco Chávez --- src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 2 +- src/pages/inbox/report/ReportActionItem.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index b63bab9296f20..a4bd5dd864697 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -770,7 +770,7 @@ const ContextMenuActions: ContextMenuAction[] = [ movedFromReport, movedToReport, policyTags, - currentUserLogin: currentUserPersonalDetails?.login ?? '', + currentUserLogin: currentUserPersonalDetails?.email ?? '', }); Clipboard.setString(modifyExpenseMessage); } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 3294c777d923e..eb03284bf1374 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -99,7 +99,7 @@ function ReportActionItem({ const originalReportID = useOriginalReportID(reportID, action); const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; const isOriginalReportArchived = useReportIsArchived(originalReportID); - const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails(); + const {accountID: currentUserAccountID, email: currentUserEmail} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); // When an expense is moved from a self-DM to a workspace, the report's policyID is temporarily // set to a fake placeholder (CONST.POLICY.OWNER_EMAIL_FAKE). Looking up POLICY_TAGS with that @@ -175,7 +175,7 @@ function ReportActionItem({ movedFromReport, movedToReport, policyTags: policyTags ?? CONST.POLICY.DEFAULT_TAG_LIST, - currentUserLogin: currentUserLogin ?? '', + currentUserLogin: currentUserEmail ?? '', })} getTransactionsWithReceipts={getTransactionsWithReceipts} clearError={clearError}