diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 3067f4cc609ae..36c4f13803f78 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -165,6 +165,8 @@ function getMovedFromOrToReportMessage( movedFromReport: OnyxEntry | undefined, movedToReport: OnyxEntry | undefined, currentUserLogin: string, + // TODO: This will be required eventually. Refactor issue: https://github.com/Expensify/App/issues/66411 + conciergeReportID?: string, ): string | undefined { if (movedToReport) { return getForExpenseMovedFromSelfDM(translate, movedToReport, currentUserLogin); @@ -172,7 +174,7 @@ function getMovedFromOrToReportMessage( if (movedFromReport) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const originReportName = getReportName({report: movedFromReport}); + const originReportName = getReportName({report: movedFromReport, conciergeReportID}); return translate('iou.movedFromReport', originReportName ?? ''); } } @@ -251,6 +253,7 @@ function getForReportAction({ movedToReport, policyTags, currentUserLogin, + conciergeReportID, }: { translate: LocalizedTranslate; reportAction: OnyxEntry; @@ -262,12 +265,14 @@ function getForReportAction({ // See https://github.com/Expensify/App/pull/75562 policyTags?: OnyxEntry; currentUserLogin: string; + // TODO: This will be required eventually. Refactor issue: https://github.com/Expensify/App/issues/66411 + conciergeReportID?: string; }): string { if (!isModifiedExpenseAction(reportAction)) { return ''; } - const movedFromOrToReportMessage = getMovedFromOrToReportMessage(translate, movedFromReport, movedToReport, currentUserLogin); + const movedFromOrToReportMessage = getMovedFromOrToReportMessage(translate, movedFromReport, movedToReport, currentUserLogin, conciergeReportID); if (movedFromOrToReportMessage) { return movedFromOrToReportMessage; } diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index 71819a40aaab2..dc2fe2ffe3315 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -142,6 +142,7 @@ export default { policyTags, policy, currentUserLogin, + conciergeReportID, }: LocalNotificationModifiedExpensePushParams) { const title = reportAction.person?.map((f) => f.text).join(', ') ?? ''; const bodyWithHTML = getForReportAction({ @@ -153,6 +154,7 @@ export default { movedToReport, policyTags, currentUserLogin, + conciergeReportID, }); // Strip HTML tags for plain text notification body const body = getTextFromHtml(bodyWithHTML); diff --git a/src/libs/Notification/LocalNotification/index.ts b/src/libs/Notification/LocalNotification/index.ts index fcf855898381c..a398ad1238bcd 100644 --- a/src/libs/Notification/LocalNotification/index.ts +++ b/src/libs/Notification/LocalNotification/index.ts @@ -35,11 +35,22 @@ function showUpdateAvailableNotification() { BrowserNotifications.pushUpdateAvailableNotification(); } -function showModifiedExpenseNotification({report, reportAction, movedFromReport, movedToReport, onClick, currentUserLogin}: LocalNotificationModifiedExpenseParams) { +function showModifiedExpenseNotification({report, reportAction, movedFromReport, movedToReport, onClick, currentUserLogin, conciergeReportID}: LocalNotificationModifiedExpenseParams) { const policyID = report.policyID; const policyTags = policyID ? allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] : undefined; const policy = policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined; - BrowserNotifications.pushModifiedExpenseNotification({report, reportAction, movedFromReport, movedToReport, onClick, usesIcon: true, policyTags, policy, currentUserLogin}); + BrowserNotifications.pushModifiedExpenseNotification({ + report, + reportAction, + movedFromReport, + movedToReport, + onClick, + usesIcon: true, + policyTags, + policy, + currentUserLogin, + conciergeReportID, + }); } function clearReportNotifications(reportID: string | undefined) { diff --git a/src/libs/Notification/LocalNotification/types.ts b/src/libs/Notification/LocalNotification/types.ts index cb0b6805ebde8..dd701cb5ae45c 100644 --- a/src/libs/Notification/LocalNotification/types.ts +++ b/src/libs/Notification/LocalNotification/types.ts @@ -22,6 +22,7 @@ type LocalNotificationModifiedExpenseParams = { movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; currentUserLogin: string; + conciergeReportID: string | undefined; }; type LocalNotificationModifiedExpensePushParams = LocalNotificationModifiedExpenseParams & { diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index f70722b296bb1..866729c4181cf 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4095,7 +4095,7 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE) { const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; - LocalNotification.showModifiedExpenseNotification({report, reportAction, onClick, movedFromReport, movedToReport, currentUserLogin}); + LocalNotification.showModifiedExpenseNotification({report, reportAction, onClick, movedFromReport, movedToReport, currentUserLogin, conciergeReportID}); } else { LocalNotification.showCommentNotification(report, reportAction, onClick, conciergeReportID); } diff --git a/tests/unit/showReportActionNotificationTest.ts b/tests/unit/showReportActionNotificationTest.ts new file mode 100644 index 0000000000000..aed1f1de98e21 --- /dev/null +++ b/tests/unit/showReportActionNotificationTest.ts @@ -0,0 +1,151 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, jest} from '@jest/globals'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import * as Report from '@src/libs/actions/Report'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@libs/ActiveClientManager', () => ({ + isClientTheLeader: jest.fn(() => true), + isReady: jest.fn(() => Promise.resolve()), + init: jest.fn(), +})); + +const mockShowModifiedExpenseNotification = jest.fn(); +const mockShowCommentNotification = jest.fn(); +jest.mock('@libs/Notification/LocalNotification', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + showModifiedExpenseNotification: (...args: unknown[]) => mockShowModifiedExpenseNotification(...args), + showCommentNotification: (...args: unknown[]) => mockShowCommentNotification(...args), + showUpdateAvailableNotification: jest.fn(), + clearReportNotifications: jest.fn(), + }, +})); + +jest.mock('@libs/Navigation/Navigation', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + getTopmostReportId: jest.fn(() => 'other-report-id'), + navigate: jest.fn(), + }, +})); + +jest.mock('@libs/Visibility', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + isVisible: jest.fn(() => false), + hasFocus: jest.fn(() => false), + }, +})); + +const CURRENT_USER_ACCOUNT_ID = 1; +const CURRENT_USER_LOGIN = 'test@user.com'; +const REPORT_ID = '100'; +const OTHER_USER_ACCOUNT_ID = 2; +const CONCIERGE_REPORT_ID = '42'; + +describe('showReportActionNotification', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(() => { + mockShowModifiedExpenseNotification.mockClear(); + mockShowCommentNotification.mockClear(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + afterEach(() => { + return Onyx.clear(); + }); + + async function setupReport() { + await Onyx.set(ONYXKEYS.SESSION, {accountID: CURRENT_USER_ACCOUNT_ID, email: CURRENT_USER_LOGIN}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + participants: { + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + }, + }); + await waitForBatchedUpdates(); + } + + it('passes conciergeReportID to showModifiedExpenseNotification for MODIFIED_EXPENSE actions', async () => { + await setupReport(); + + const reportAction = { + reportActionID: 'action1', + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + actorAccountID: OTHER_USER_ACCOUNT_ID, + created: '2026-01-01 00:00:00.000', + message: [{type: 'COMMENT', html: 'expense modified', text: 'expense modified'}], + person: [{type: 'TEXT', style: 'strong', text: 'Other User'}], + }; + + Report.showReportActionNotification( + REPORT_ID, + reportAction as Parameters[1], + CURRENT_USER_ACCOUNT_ID, + CURRENT_USER_LOGIN, + CONCIERGE_REPORT_ID, + ); + await waitForBatchedUpdates(); + + expect(mockShowModifiedExpenseNotification).toHaveBeenCalledTimes(1); + const callArgs = mockShowModifiedExpenseNotification.mock.calls.at(0)?.at(0) as Record; + expect(callArgs.conciergeReportID).toBe(CONCIERGE_REPORT_ID); + expect(mockShowCommentNotification).not.toHaveBeenCalled(); + }); + + it('passes undefined conciergeReportID to showModifiedExpenseNotification when not provided', async () => { + await setupReport(); + + const reportAction = { + reportActionID: 'action2', + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + actorAccountID: OTHER_USER_ACCOUNT_ID, + created: '2026-01-01 00:00:00.000', + message: [{type: 'COMMENT', html: 'expense modified', text: 'expense modified'}], + person: [{type: 'TEXT', style: 'strong', text: 'Other User'}], + }; + + Report.showReportActionNotification(REPORT_ID, reportAction as Parameters[1], CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, undefined); + await waitForBatchedUpdates(); + + expect(mockShowModifiedExpenseNotification).toHaveBeenCalledTimes(1); + const callArgs = mockShowModifiedExpenseNotification.mock.calls.at(0)?.at(0) as Record; + expect(callArgs.conciergeReportID).toBeUndefined(); + expect(mockShowCommentNotification).not.toHaveBeenCalled(); + }); + + it('routes non-MODIFIED_EXPENSE actions to showCommentNotification', async () => { + await setupReport(); + + const reportAction = { + reportActionID: 'action3', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + actorAccountID: OTHER_USER_ACCOUNT_ID, + created: '2026-01-01 00:00:00.000', + message: [{type: 'COMMENT', html: 'hello', text: 'hello'}], + person: [{type: 'TEXT', style: 'strong', text: 'Other User'}], + }; + + Report.showReportActionNotification( + REPORT_ID, + reportAction as Parameters[1], + CURRENT_USER_ACCOUNT_ID, + CURRENT_USER_LOGIN, + CONCIERGE_REPORT_ID, + ); + await waitForBatchedUpdates(); + + expect(mockShowCommentNotification).toHaveBeenCalledTimes(1); + expect(mockShowModifiedExpenseNotification).not.toHaveBeenCalled(); + }); +});