Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/libs/ModifiedExpenseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,16 @@ function getMovedFromOrToReportMessage(
movedFromReport: OnyxEntry<Report> | undefined,
movedToReport: OnyxEntry<Report> | 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);
}

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 ?? '');
}
}
Expand Down Expand Up @@ -251,6 +253,7 @@ function getForReportAction({
movedToReport,
policyTags,
currentUserLogin,
conciergeReportID,
}: {
translate: LocalizedTranslate;
reportAction: OnyxEntry<ReportAction>;
Expand All @@ -262,12 +265,14 @@ function getForReportAction({
// See https://github.com/Expensify/App/pull/75562
policyTags?: OnyxEntry<PolicyTagLists>;
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export default {
policyTags,
policy,
currentUserLogin,
conciergeReportID,
}: LocalNotificationModifiedExpensePushParams) {
const title = reportAction.person?.map((f) => f.text).join(', ') ?? '';
const bodyWithHTML = getForReportAction({
Expand All @@ -153,6 +154,7 @@ export default {
movedToReport,
policyTags,
currentUserLogin,
conciergeReportID,
});
// Strip HTML tags for plain text notification body
const body = getTextFromHtml(bodyWithHTML);
Expand Down
15 changes: 13 additions & 2 deletions src/libs/Notification/LocalNotification/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/libs/Notification/LocalNotification/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type LocalNotificationModifiedExpenseParams = {
movedFromReport?: OnyxEntry<Report>;
movedToReport?: OnyxEntry<Report>;
currentUserLogin: string;
conciergeReportID: string | undefined;
};

type LocalNotificationModifiedExpensePushParams = LocalNotificationModifiedExpenseParams & {
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@
// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection<ReportActions> = {};

Onyx.connect({

Check warning on line 376 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
if (!key || !actions) {
Expand All @@ -385,7 +385,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 388 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -394,7 +394,7 @@
});

let allPersonalDetails: OnyxEntry<PersonalDetailsList> = {};
Onyx.connect({

Check warning on line 397 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand All @@ -412,7 +412,7 @@
});

let onboarding: OnyxEntry<Onboarding>;
Onyx.connect({

Check warning on line 415 in src/libs/actions/Report/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_ONBOARDING,
callback: (val) => {
if (Array.isArray(val)) {
Expand Down Expand Up @@ -4095,7 +4095,7 @@
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);
}
Expand Down
151 changes: 151 additions & 0 deletions tests/unit/showReportActionNotificationTest.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Report.showReportActionNotification>[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<string, unknown>;
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<typeof Report.showReportActionNotification>[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<string, unknown>;
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<typeof Report.showReportActionNotification>[1],
CURRENT_USER_ACCOUNT_ID,
CURRENT_USER_LOGIN,
CONCIERGE_REPORT_ID,
);
await waitForBatchedUpdates();

expect(mockShowCommentNotification).toHaveBeenCalledTimes(1);
expect(mockShowModifiedExpenseNotification).not.toHaveBeenCalled();
});
});
Loading