diff --git a/src/hooks/useOneTransactionThreadReportID.tsx b/src/hooks/useOneTransactionThreadReportID.tsx new file mode 100644 index 000000000000..8feb8c35d245 --- /dev/null +++ b/src/hooks/useOneTransactionThreadReportID.tsx @@ -0,0 +1,19 @@ +import {getOneTransactionThreadReportID} from '@libs/ReportActionsUtils'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +import useNetwork from './useNetwork'; +import useOnyx from './useOnyx'; + +function useOneTransactionThreadReportID(reportID: string | undefined) { + const {isOffline} = useNetwork(); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`); + const [oneTransactionThreadReportID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + selector: (actions) => getOneTransactionThreadReportID(report, chatReport, actions, isOffline), + }); + + return oneTransactionThreadReportID; +} + +export default useOneTransactionThreadReportID; diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 7c2b5428324b..336656f31ed2 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -5,9 +5,11 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useHasActiveAdminPolicies from '@hooks/useHasActiveAdminPolicies'; import useLastWorkspaceNumber from '@hooks/useLastWorkspaceNumber'; import useLocalize from '@hooks/useLocalize'; +import useOneTransactionThreadReportID from '@hooks/useOneTransactionThreadReportID'; import useOnyx from '@hooks/useOnyx'; import useReconcileHighContrastIntent from '@hooks/useReconcileHighContrastIntent'; import useReportAttributes from '@hooks/useReportAttributes'; +import useRootNavigationState from '@hooks/useRootNavigationState'; import {init, isClientTheLeader} from '@libs/ActiveClientManager'; import Log from '@libs/Log'; @@ -38,13 +40,18 @@ import type {ReportAttributesDerivedValue} from '@src/types/onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import {useEffect, useRef} from 'react'; -function initializePusher(currentUserAccountID?: number, currentUserEmail?: string, getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined) { +function initializePusher( + currentUserAccountID: number | undefined, + currentUserEmail: string | undefined, + getTopmostOneTransactionThreadReportID: () => string | undefined, + getReportAttributes: () => ReportAttributesDerivedValue['reports'] | undefined, +) { return Pusher.init({ appKey: CONFIG.PUSHER.APP_KEY, cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, }).then(() => { - User.subscribeToUserEvents(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', getReportAttributes); + User.subscribeToUserEvents(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', getTopmostOneTransactionThreadReportID, getReportAttributes); }); } @@ -82,6 +89,15 @@ function AuthScreensInitHandler() { useReconcileHighContrastIntent(); + const topmostReportID = useRootNavigationState(Navigation.getTopmostReportId); + const topmostOneTransactionThreadReportID = useOneTransactionThreadReportID(topmostReportID); + // We use a ref so the Pusher callback (registered once on mount) always reads the latest value without re-subscribing. + const topmostOneTransactionThreadReportIDRef = useRef(topmostOneTransactionThreadReportID); + + useEffect(() => { + topmostOneTransactionThreadReportIDRef.current = topmostOneTransactionThreadReportID; + }, [topmostOneTransactionThreadReportID]); + useEffect(() => { registerPusherReinitializeHandler(({accountID, email}: PusherReinitializeHandlerParams = {}) => { const currentAccountID = accountID ?? session?.accountID; @@ -91,7 +107,12 @@ function AuthScreensInitHandler() { return Promise.resolve(); } - return initializePusher(currentAccountID, currentEmail, () => reportAttributesRef.current); + return initializePusher( + currentAccountID, + currentEmail, + () => topmostOneTransactionThreadReportIDRef.current, + () => reportAttributesRef.current, + ); }); return () => { @@ -104,7 +125,7 @@ function AuthScreensInitHandler() { return; } // This means sign in in RHP was successful, so we can subscribe to user events - initializePusher(session?.accountID, session?.email, () => reportAttributesRef.current); + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current); }, [session?.accountID, session?.email]); useEffect(() => { @@ -127,7 +148,7 @@ function AuthScreensInitHandler() { }); PusherConnectionManager.init(); - initializePusher(session?.accountID, session?.email, () => reportAttributesRef.current).finally(() => { + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current).finally(() => { endSpan(CONST.TELEMETRY.SPAN_NAVIGATION.PUSHER_INIT); }); diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index 97c699180165..df6e0e34faeb 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,12 +1,16 @@ import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import {getIsOffline} from '@libs/NetworkState'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report as ReportType, ReportActions} from '@src/types/onyx'; import type {PushPayload} from '@ua/react-native-airship'; +import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -21,6 +25,22 @@ Onyx.connectWithoutView({ }, }); +// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. +let allReportActions: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (value) => (allReportActions = value), +}); + +// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. +let allReports: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (value) => (allReports = value), +}); + /** * Returns whether the given Airship notification should be shown depending on the current state of the app */ @@ -37,7 +57,12 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo shouldShow = true; } else { const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null); - shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), currentUserAccountID, reportAction, true); + const topmostReportID = Navigation.getTopmostReportId(); + const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`]; + const topmostChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`]; + const topmostReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${topmostReportID}`]; + const topmostOneTransactionThreadReportID = ReportActionUtils.getOneTransactionThreadReportID(topmostReport, topmostChatReport, topmostReportActions, getIsOffline()); + shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), topmostOneTransactionThreadReportID, currentUserAccountID, reportAction, true); } Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`); diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index dfaee0078f28..0ab4f69c4339 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4566,7 +4566,13 @@ function setIsComposerFullSize(reportID: string, isComposerFullSize: boolean) { * @param isRemote whether or not this notification is a remote push notification * @param currentUserAccountID the current user's account ID */ -function shouldShowReportActionNotification(reportID: string, currentUserAccountID: number, action: ReportAction | null = null, isRemote = false): boolean { +function shouldShowReportActionNotification( + reportID: string, + topmostOneTransactionThreadReportID: string | undefined, + currentUserAccountID: number, + action: ReportAction | null = null, + isRemote = false, +): boolean { const tag = isRemote ? '[PushNotification]' : '[LocalNotification]'; const topmostReportID = Navigation.getTopmostReportId(); @@ -4602,14 +4608,7 @@ function shouldShowReportActionNotification(reportID: string, currentUserAccount } // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification. - const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`]; - const topmostReportActions = allReportActions?.[`${topmostReport?.reportID}`]; - const chatTopmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`]; - if ( - reportID === ReportActionsUtils.getOneTransactionThreadReportID(topmostReport, chatTopmostReport, topmostReportActions, isOfflineNetwork()) && - Visibility.isVisible() && - Visibility.hasFocus() - ) { + if (reportID === topmostOneTransactionThreadReportID && Visibility.isVisible() && Visibility.hasFocus()) { Log.info(`${tag} No notification because the report is a transaction thread associated with the current one-transaction report`); return false; } @@ -4638,11 +4637,12 @@ function shouldShowReportActionNotification(reportID: string, currentUserAccount function showReportActionNotification( reportID: string, reportAction: ReportAction, + topmostOneTransactionThreadReportID: string | undefined, currentUserAccountID: number, currentUserLogin: string, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { - if (!shouldShowReportActionNotification(reportID, currentUserAccountID, reportAction)) { + if (!shouldShowReportActionNotification(reportID, topmostOneTransactionThreadReportID, currentUserAccountID, reportAction)) { return; } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e9f2aa29930e..8174efb6338f 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -675,6 +675,7 @@ function triggerNotifications( onyxUpdates: Array>, currentUserAccountID: number, currentUserEmail: string, + topmostOneTransactionThreadReportID: string | undefined, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { for (const update of onyxUpdates) { @@ -688,7 +689,7 @@ function triggerNotifications( for (const action of reportActions) { if (action) { // They aren't connected to a UI anywhere, it's OK to use currentUserEmail - showReportActionNotification(reportID, action, currentUserAccountID, currentUserEmail, reportAttributes); + showReportActionNotification(reportID, action, topmostOneTransactionThreadReportID, currentUserAccountID, currentUserEmail, reportAttributes); } } } @@ -915,7 +916,12 @@ function initializePusherPingPong(currentUserAccountID: number) { * Handles the newest events from Pusher where a single mega multipleEvents contains * an array of singular events all in one event */ -function subscribeToUserEvents(currentUserAccountID: number, currentUserEmail: string, getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined) { +function subscribeToUserEvents( + currentUserAccountID: number, + currentUserEmail: string, + getTopmostOneTransactionThreadReportID: () => string | undefined, + getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined, +) { // If we don't have the user's accountID yet (because the app isn't fully setup yet) we can't subscribe so return early if (!currentUserAccountID) { return; @@ -970,7 +976,7 @@ function subscribeToUserEvents(currentUserAccountID: number, currentUserEmail: s } const onyxUpdatePromise = Onyx.update(pushJSON).then(() => { - triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getReportAttributes?.()); + triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getTopmostOneTransactionThreadReportID(), getReportAttributes?.()); }); // Return a promise when Onyx is done updating so that the OnyxUpdatesManager can properly apply all diff --git a/tests/actions/IOU/RequestMoneyTest.ts b/tests/actions/IOU/RequestMoneyTest.ts index 29c54cf15cf8..b28794b97f08 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -1847,7 +1847,7 @@ describe('actions/IOU', () => { // Given a test user is signed in with Onyx setup and some initial data await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); diff --git a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts index 86661ad5afef..2430ee36b64f 100644 --- a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts +++ b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts @@ -160,7 +160,7 @@ describe('actions/IOU/DeleteMoneyRequest', () => { // Given a test user is signed in with Onyx setup and some initial data await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); diff --git a/tests/actions/IOUTest/TrackExpenseTest.ts b/tests/actions/IOUTest/TrackExpenseTest.ts index 208843caddfb..c6a0b756b4a8 100644 --- a/tests/actions/IOUTest/TrackExpenseTest.ts +++ b/tests/actions/IOUTest/TrackExpenseTest.ts @@ -2216,7 +2216,7 @@ describe('actions/IOU/TrackExpense', () => { // Given a test user is signed in with Onyx setup and some initial data await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); @@ -2520,7 +2520,7 @@ describe('actions/IOU/TrackExpense', () => { PusherHelper.setup(); await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index ae85aa79666f..def7411b4ae1 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -258,7 +258,7 @@ describe('actions/Report', () => { // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) @@ -560,7 +560,7 @@ describe('actions/Report', () => { .then(waitForNetworkPromises) .then(() => { // Given a test user that is subscribed to Pusher events - User.subscribeToUserEvents(USER_1_ACCOUNT_ID, USER_1_LOGIN, undefined); + User.subscribeToUserEvents(USER_1_ACCOUNT_ID, USER_1_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(USER_1_LOGIN, USER_1_ACCOUNT_ID)) @@ -914,7 +914,7 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(waitForBatchedUpdates) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => { @@ -933,7 +933,7 @@ describe('actions/Report', () => { }) .then(() => { // Ensure we show a notification for this new report action - expect(Report.showReportActionNotification).toBeCalledWith(REPORT_ID, REPORT_ACTION, TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + expect(Report.showReportActionNotification).toBeCalledWith(REPORT_ID, REPORT_ACTION, undefined, TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); }); }); @@ -972,7 +972,7 @@ describe('actions/Report', () => { // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) @@ -1110,7 +1110,7 @@ describe('actions/Report', () => { // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) diff --git a/tests/ui/AuthScreensInitHandlerTest.tsx b/tests/ui/AuthScreensInitHandlerTest.tsx index 7912d19ed010..980956740797 100644 --- a/tests/ui/AuthScreensInitHandlerTest.tsx +++ b/tests/ui/AuthScreensInitHandlerTest.tsx @@ -49,6 +49,7 @@ jest.mock('@libs/Navigation/Navigation', () => ({ isActiveRoute: jest.fn(() => false), navigate: jest.fn(), getActiveRouteWithoutParams: jest.fn(() => ''), + getTopmostReportId: jest.fn(() => undefined), isNavigationReady: jest.fn(() => Promise.resolve()), setNavigationActionToMicrotaskQueue: jest.fn(() => Promise.resolve()), }, @@ -156,7 +157,7 @@ describe('AuthScreensInitHandler', () => { await waitForBatchedUpdatesWithAct(); expect(mockedPusherInit).toHaveBeenCalled(); - expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function)); + expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function), expect.any(Function)); }); it('calls subscribeToUserEvents from sign-in modal effect when SIGN_IN_MODAL is active', async () => { @@ -170,7 +171,7 @@ describe('AuthScreensInitHandler', () => { // Both mount effect AND sign-in modal effect fire → 2 calls expect(subscribeToUserEvents).toHaveBeenCalledTimes(2); - expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function)); + expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function), expect.any(Function)); }); it('getter passed to subscribeToUserEvents returns report attributes when available', async () => { @@ -185,7 +186,7 @@ describe('AuthScreensInitHandler', () => { const mockCalls = (subscribeToUserEvents as jest.Mock).mock.calls; const firstCallArgs = mockCalls.at(0) as unknown[]; - const getter = firstCallArgs.at(2) as () => unknown; + const getter = firstCallArgs.at(3) as () => unknown; expect(getter()).toEqual(mockReports); }); @@ -199,7 +200,7 @@ describe('AuthScreensInitHandler', () => { const mockCalls = (subscribeToUserEvents as jest.Mock).mock.calls; const firstCallArgs = mockCalls.at(0) as unknown[]; - const getter = firstCallArgs.at(2) as () => unknown; + const getter = firstCallArgs.at(3) as () => unknown; expect(getter()).toBeUndefined(); }); diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 0088d78b5281..5c06c8ad37cc 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -191,7 +191,7 @@ function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Pro }) .then(async () => TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A')) .then(() => { - subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined); + subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, () => {}, undefined); return waitForBatchedUpdates(); }) .then(async () => { diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 79994f730d38..375441a1e3ef 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -231,7 +231,7 @@ async function signInAndGetApp(): Promise { await waitForBatchedUpdatesWithAct(); // Start listening for pusher events after navigation settles. - subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined); + subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, () => {}, undefined); await waitForBatchedUpdates(); await Promise.all([ diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index b4743400a1a0..0561b9eceee4 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -190,7 +190,7 @@ async function signInAndGetAppWithUnreadChat(): Promise { renderAppOnce(); await waitForBatchedUpdatesWithAct(); - subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined); + subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, () => {}, undefined); await waitForBatchedUpdates(); diff --git a/tests/unit/showReportActionNotificationTest.ts b/tests/unit/showReportActionNotificationTest.ts index d86dbf094fb4..942c7889bb9c 100644 --- a/tests/unit/showReportActionNotificationTest.ts +++ b/tests/unit/showReportActionNotificationTest.ts @@ -91,9 +91,10 @@ describe('showReportActionNotification', () => { Report.showReportActionNotification( REPORT_ID, reportAction as Parameters[1], + undefined, CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, - REPORT_ATTRIBUTES as Parameters[4], + REPORT_ATTRIBUTES as Parameters[5], ); await waitForBatchedUpdates(); @@ -115,7 +116,14 @@ describe('showReportActionNotification', () => { person: [{type: 'TEXT', style: 'strong', text: 'Other User'}], }; - Report.showReportActionNotification(REPORT_ID, reportAction as Parameters[1], CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, undefined); + Report.showReportActionNotification( + REPORT_ID, + reportAction as Parameters[1], + undefined, + CURRENT_USER_ACCOUNT_ID, + CURRENT_USER_LOGIN, + undefined, + ); await waitForBatchedUpdates(); expect(mockShowModifiedExpenseNotification).toHaveBeenCalledTimes(1); @@ -139,9 +147,10 @@ describe('showReportActionNotification', () => { Report.showReportActionNotification( REPORT_ID, reportAction as Parameters[1], + undefined, CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, - REPORT_ATTRIBUTES as Parameters[4], + REPORT_ATTRIBUTES as Parameters[5], ); await waitForBatchedUpdates(); diff --git a/tests/unit/useOneTransactionThreadReportIDTest.ts b/tests/unit/useOneTransactionThreadReportIDTest.ts new file mode 100644 index 000000000000..fc16fecdc12e --- /dev/null +++ b/tests/unit/useOneTransactionThreadReportIDTest.ts @@ -0,0 +1,129 @@ +import {act, renderHook} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import useOneTransactionThreadReportID from '@hooks/useOneTransactionThreadReportID'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction} from '@src/types/onyx'; + +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +const REPORT_ID = 'report1'; +const CHAT_REPORT_ID = 'chat1'; + +const report: Report = { + reportID: REPORT_ID, + chatReportID: CHAT_REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, +}; + +const chatReport: Report = { + reportID: CHAT_REPORT_ID, + type: CONST.REPORT.TYPE.CHAT, +}; + +function createIOUAction(reportActionID: string, transactionID: string, childReportID?: string): ReportAction { + return { + reportActionID, + childReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2025-02-14 08:12:05.165', + originalMessage: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: 10402, + currency: CONST.CURRENCY.USD, + IOUTransactionID: transactionID, + }, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + html: '$104.02 expense', + text: '$104.02 expense', + }, + ], + }; +} + +describe('useOneTransactionThreadReportID', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CHAT_REPORT_ID}`, chatReport); + }); + + afterEach(() => { + return Onyx.clear(); + }); + + it('returns undefined when reportID is undefined', async () => { + const {result} = renderHook(() => useOneTransactionThreadReportID(undefined), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); + + it('returns the childReportID when the report has exactly one IOU transaction action', async () => { + const action = createIOUAction('action1', 'transaction1', 'thread1'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[action.reportActionID]: action}); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBe('thread1'); + }); + + it('returns CONST.FAKE_REPORT_ID when the single IOU action has no childReportID', async () => { + const action = createIOUAction('action1', 'transaction1'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[action.reportActionID]: action}); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBe(CONST.FAKE_REPORT_ID); + }); + + it('returns undefined when the report has more than one IOU transaction action', async () => { + const action1 = createIOUAction('action1', 'transaction1', 'thread1'); + const action2 = createIOUAction('action2', 'transaction2', 'thread2'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [action1.reportActionID]: action1, + [action2.reportActionID]: action2, + }); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when there are no report actions', async () => { + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when the report is not an IOU, Expense, or Invoice report', async () => { + const action = createIOUAction('action1', 'transaction1', 'thread1'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {...report, type: CONST.REPORT.TYPE.CHAT}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[action.reportActionID]: action}); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); +});