Repository navigation
[Due for payment 2026-08-08] [$250] Notification appears when user is focused on the report #95711
Description
Activity
- addedDailyKSv2KSv2BugSomething is broken. Auto assigns a BugZero manager.Something is broken. Auto assigns a BugZero manager.
on Jul 9, 2026 While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/4c61004453dacadaa5a21083ce388661647a65d0cc0abede2f65f8e42bb3b7e1Issue Analysis
This is a regression from #95318 (deployed to staging in the reported version
9.4.32-0) — QA already flagged step 4 failing on that PR. The suppression code #95318 added is inmainbut it doesn't actually fire in this flow.Root Cause: The "don't notify me for the transaction thread of the report I'm viewing" check compares the incoming action's real transaction-thread reportID against
topmostOneTransactionThreadReportIDatReport/index.ts:4616. For User A (a viewer who never created the expense and never opened the thread), the single IOU action has nochildReportID, sogetOneTransactionThreadReportIDfalls back to the sentinel stringCONST.FAKE_REPORT_ID('FAKE_REPORT_ID').'FAKE_REPORT_ID' === '<real thread id>'is always false → the check never suppresses → the notification shows.Recommended Fix: Don't rely on the viewer's (often-empty)
childReportID. Instead, suppress by checking the incoming thread's own parent link: if the report for the incomingreportIDis a transaction thread whoseparentReportIDequalstopmostReportID(the one-transaction report being viewed), suppress. The thread report always carriesparentReportIDfrom the server, so this doesn't depend on the optimisticchildReportID. At minimum, also guard the existing comparison so aFAKE_REPORT_IDvalue can never be treated as a match.Investigation details & evidence
Data flow (local Pusher path):
- User A focuses the one-transaction expense report →
getTopmostReportId()= expenseReportID. useOneTransactionThreadReportID(expenseReportID)finds the single IOU action correctly, but returnsiouAction.childReportID ?? CONST.FAKE_REPORT_ID→ReportActionsUtils.ts:1972. On a viewer's clientchildReportIDis unset (it's only set optimistically for the expense creator —ReportUtils.ts:8894), so it returns'FAKE_REPORT_ID'.- User B posts on the real transaction thread →
triggerNotificationspasses the real thread reportID. shouldShowReportActionNotification:realID !== expenseReportID(L4610 passes) andrealID !== 'FAKE_REPORT_ID'(L4616 passes) → returnstrue→ notification shown.
Constant:
CONST.FAKE_REPORT_ID: 'FAKE_REPORT_ID'.Why #95318's QA missed it: the tests in
tests/unit/showReportActionNotificationTest.tsalways passtopmostOneTransactionThreadReportIDasundefined, so theL4616branch is never exercised with a real incoming thread ID + theFAKE_REPORT_IDfallback. A regression test should set up a one-transaction report as topmost (viewer has nochildReportID), an incoming action on the real thread ID with visibility/focus true, and assert no notification fires.Ruled out: wrong topmost report (the expense report's actions are read correctly), wrong incoming reportID (it's the correct real thread ID), and leader/staleness (leader check already passed; ref update is a tick behind at most — secondary).
Confidence: High — deterministic code path, verified end-to-end with
file:lineevidence. Since this is a fresh regression from a just-merged PR,bernhardoj(PR author) may be best placed to fold the fix into a follow-up.
Next Steps for Contributor+ team:
To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/95711)to create a draft PR.
To refine:@MelvinBot <your feedback>
To reject: Explain why you are rejecting Melvin's proposal.- User A focuses the one-transaction expense report →
- addedExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributor
on Jul 9, 2026 🚨 Edited by proposal-police: This proposal was edited at 2026-07-20 09:16:56 UTC.
Proposal
What is the root cause of that problem?
The notification suppression path now depends on a precomputed
topmostOneTransactionThreadReportID:AuthScreensInitHandlerderives it from the focused report and passes it to Pusher callbacks (), then Pusher callsApp/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
Lines 92 to 99 in 665e743
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]); triggerNotifications()after applying the incoming Onyx update and forwards that cached ID intoshowReportActionNotification()(,Lines 674 to 692 in 665e743
function triggerNotifications<TKey extends OnyxKey>( onyxUpdates: Array<OnyxServerUpdate<TKey>>, currentUserAccountID: number, currentUserEmail: string, topmostOneTransactionThreadReportID: string | undefined, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { for (const update of onyxUpdates) { if (!update.shouldNotify && !update.shouldShowPushNotification) { continue; } const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); const reportActions = Object.values((update.value as OnyxCollection<ReportAction>) ?? {}); for (const action of reportActions) { if (action) { // They aren't connected to a UI anywhere, it's OK to use currentUserEmail showReportActionNotification(reportID, action, topmostOneTransactionThreadReportID, currentUserAccountID, currentUserEmail, reportAttributes); ).Lines 978 to 980 in 665e743
const onyxUpdatePromise = Onyx.update(pushJSON).then(() => { triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getTopmostOneTransactionThreadReportID(), getReportAttributes?.()); }); shouldShowReportActionNotification()only suppresses a transaction-thread notification when the incomingreportIDexactly equals that precomputed thread ID ().App/src/libs/actions/Report/index.ts
Lines 4601 to 4610 in 665e743
reportID: string, topmostOneTransactionThreadReportID: string | undefined, currentUserAccountID: number, action: ReportAction | null = null, isRemote = false, ): boolean { const tag = isRemote ? '[PushNotification]' : '[LocalNotification]'; const topmostReportID = Navigation.getTopmostReportId(); // Due to payload size constraints, some push notifications may have their report action stripped The problem is that the focused report ID used to compute
topmostOneTransactionThreadReportIDcan be incorrect on wide/RHP flows.AuthScreensInitHandlercurrently derives the focused report fromNavigation.getTopmostReportId(), but that helper only reads the report fromREPORTS_SPLIT_NAVIGATOR. When a one-transaction expense report is visible in the RHP/super-wide RHP, the central pane can still hold the parent chat, soNavigation.getTopmostReportId()can return the background chat report ID instead of the expense report ID that the user is actually viewing. This matches the C+ feedback that the report ID is incorrect before reload.The read-only/composer-blocked case after reload looks like a separate permission issue, as discussed in the latest comments. However, the notification bug still applies to the valid expense-in-DM flow, where the user is focused on the related one-transaction report/thread context but the notification suppression still uses the wrong focused report source. Because the global notification path can start from the background split report instead of the visible expense report, it can compute
undefinedor an unrelatedtopmostOneTransactionThreadReportID, so a new message on the related transaction thread is treated as a different report and the browser notification is shown.What changes do you think we should make in order to solve the problem?
First, derive the currently viewed report ID from the visible report context, not only from
Navigation.getTopmostReportId(). When a report is open in the RHP/super-wide RHP, prefer that report ID; otherwise fall back to the currentNavigation.getTopmostReportId()behavior. Then pass this corrected focused report ID intouseOneTransactionThreadReportID().Also update the direct foreground push-notification derivation in
shouldShowPushNotification()to use the corrected currently viewed report ID before derivingtopmostOneTransactionThreadReportID(). That keeps local web notifications and native foreground push notifications aligned.shouldShow = true; } else { const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null); 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}`]; Use the same corrected currently viewed report ID in
shouldShowReportActionNotification()for the direct current-report comparison, so both notification suppression checks use the same source of truth for the report the user is actually focused on.What alternative solutions did you explore? (Optional)
I considered broadening
shouldShowReportActionNotification()to suppress any notification whose report is a child of the current report, but that would incorrectly hide transaction-thread notifications for multi-transaction reports where the thread message is not actually visible in the parent report. The safer fix is to make the “currently viewed report” source of truth use the correct visible report ID for notification suppression.I also considered changing
Navigation.getTopmostReportId()globally, but that helper is used by many unrelated callers that may intentionally want the split-navigator report. Keeping the RHP-first behavior scoped to notification suppression avoids changing broader navigation semantics.Fixed Demo:
Screencast.From.2026-07-10.02-22-09.mp4
- addedHelp WantedApply this label when an issue is open to proposals by contributorsApply this label when an issue is open to proposals by contributors
on Jul 9, 2026 dilshodmackbook-sketch commented
on Jul 9, 2026 ContributorMore actions🚨 Edited by proposal-police: This proposal was edited at 2026-07-10 13:12:53 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
When User A is focused on a one-transaction expense report and User B sends a message on that report's transaction thread, a notification is shown to User A even though they are actively viewing the report.
What is the root cause of that problem?
shouldShowReportActionNotificationdecides whether the user is currently viewing the relevant report using two "topmost report" values, and both of them resolve throughNavigation.getTopmostReportId():- directly, for the "current report" check:
App/src/libs/actions/Report/index.ts
Line 4608 in 884bbe4
const topmostReportID = Navigation.getTopmostReportId(); App/src/libs/actions/Report/index.ts
Lines 4635 to 4645 in 884bbe4
// If we are currently viewing this report do not show a notification. if (reportID === topmostReportID && Visibility.isVisible() && Visibility.hasFocus()) { Log.info(`${tag} No notification because it was a comment for the current report`); return false; } // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification. 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; } - indirectly, for the one-transaction-thread check:
AuthScreensInitHandlerderivestopmostOneTransactionThreadReportIDfromuseRootNavigationState(Navigation.getTopmostReportId)and hands it to the Pusher callback
App/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
Lines 94 to 95 in 884bbe4
const topmostReportID = useRootNavigationState(Navigation.getTopmostReportId); const topmostOneTransactionThreadReportID = useOneTransactionThreadReportID(topmostReportID); getTopmostReportIdcomes fromgetTopmostReportParams, which only ever inspectsSCREENS.REPORTscreens insideREPORTS_SPLIT_NAVIGATOR:App/src/libs/Navigation/helpers/getTopmostReportParams.ts
Lines 19 to 42 in 884bbe4
function getTopmostReportParams(state: State): ReportsSplitNavigatorParamList[typeof SCREENS.REPORT] | undefined { if (!state) { return; } let topmostReportsSplitNavigator = state.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); if (!topmostReportsSplitNavigator) { const rootTab = state.routes?.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); topmostReportsSplitNavigator = rootTab?.state?.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); } if (!topmostReportsSplitNavigator) { return; } const topmostReport = topmostReportsSplitNavigator.state?.routes.findLast((route) => route.name === SCREENS.REPORT); if (!topmostReport) { return; } return topmostReport?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]; } But a one-transaction expense report is normally not viewed on that screen:
- On wide layouts (the repro platforms: Windows/Chrome, macOS/Chrome), opening the expense report from the chat opens it in the super-wide RHP (
SCREENS.RIGHT_MODAL.EXPENSE_REPORT/SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT), while the central pane underneath still holds the parent chat. In this stategetTopmostReportId()keeps returning the parent chat'sreportID— a value that is wrong, not just missing.
App/src/components/WideRHPContextProvider/WIDE_RIGHT_MODALS.ts
Lines 8 to 10 in 884bbe4
const WIDE_RIGHT_MODALS = new Set<string>([SCREENS.RIGHT_MODAL.SEARCH_REPORT]); const SUPER_WIDE_RIGHT_MODALS = new Set<string>([SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT]); const ALL_WIDE_RIGHT_MODALS = new Set<string>([...WIDE_RIGHT_MODALS, ...SUPER_WIDE_RIGHT_MODALS]); - When the report is opened from the Reports tab, it renders on
SCREENS.SEARCH.MONEY_REQUEST_REPORTinsideSEARCH_FULLSCREEN_NAVIGATOR, wheregetTopmostReportId()returnsundefined.
In both cases
useOneTransactionThreadReportID(topmostReportID)is fed the wrong report (the parent chat, orundefined), so it returnsundefined, thereportID === topmostOneTransactionThreadReportIDcheck never matches, and the notification fires even though User A is focused on the report. The Airship push path has the same blind spot, since it computestopmostOneTransactionThreadReportIDfromNavigation.getTopmostReportId()too: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); This also means the bug is not really a regression of #95318: both before and after that PR, the "which report is the user viewing" resolution only ever looked at the split navigator; viewing the report in the RHP or the Reports tab was simply never covered by this code path.
What changes do you think we should make in order to solve the problem?
Add a
getCurrentlyViewedReportIDhelper inNavigation.tsthat resolves the report in visual stacking order — the RHP first, because an open wide/super-wide RHP renders above the central pane, so checkinggetTopmostReportId()first would short-circuit on the chat underneath it. I deliberately keepgetTopmostReportIditself untouched since it has ~24 call sites with navigation semantics we don't want to change.Navigation.tsalready importsALL_WIDE_RIGHT_MODALS, anduseCurrentReportIDalready sets the precedent of resolving RHP report params separately from the split navigator (getFocusedRouteReportID):App/src/hooks/useCurrentReportID.tsx
Lines 29 to 45 in 884bbe4
* Traverse the focused route at each level of the navigation state to find a reportID param. * This handles modal navigators (e.g. RightModalNavigator > ExpenseReport) that carry a reportID * in their screen params but are not part of the ReportsSplitNavigator hierarchy. */ function getFocusedRouteReportID(state: NavigationState | PartialState<NavigationState>): string | undefined { const index = state.index ?? state.routes.length - 1; const focusedRoute = state.routes[index]; if (!focusedRoute) { return; } if (focusedRoute.params && 'reportID' in focusedRoute.params && typeof focusedRoute.params.reportID === 'string') { return focusedRoute.params.reportID; } if (focusedRoute.state) { return getFocusedRouteReportID(focusedRoute.state); } } function getCurrentlyViewedReportID(state: NavigationState = navigationRef.getRootState()): string | undefined { // A report open in the wide / super-wide RHP (EXPENSE_REPORT, SEARCH_MONEY_REQUEST_REPORT, SEARCH_REPORT) // renders above the central pane, so it must be resolved first: while an RHP is open, // getTopmostReportId() still returns the central-pane chat underneath it. const lastRootRoute = state?.routes?.at(-1); if (lastRootRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { const topmostWideRHPRoute = lastRootRoute.state?.routes?.findLast((route) => ALL_WIDE_RIGHT_MODALS.has(route.name)); const rhpReportID = (topmostWideRHPRoute?.params as {reportID?: string} | undefined)?.reportID; if (rhpReportID) { return rhpReportID; } } // An expense report opened from the Reports tab renders on SCREENS.SEARCH.MONEY_REQUEST_REPORT inside // SEARCH_FULLSCREEN_NAVIGATOR (nested in TAB_NAVIGATOR), which getTopmostReportId() cannot see either. // Only consult it when the Search navigator is the focused full-screen tab. const tabState = state?.routes?.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR)?.state; const focusedTabRoute = tabState?.routes?.at(tabState.index ?? -1); if (focusedTabRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { const searchState = focusedTabRoute.state; const focusedSearchRoute = searchState?.routes?.at(searchState.index ?? -1); if (focusedSearchRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { return (focusedSearchRoute.params as {reportID?: string} | undefined)?.reportID; } } // Central-pane report — the existing behavior. return getTopmostReportId(state); }
Then use it in the three places that drive the notification "am I viewing this report" logic.
In
shouldShowReportActionNotification(also fixes the plain "current report" check for messages sent to the RHP-open report itself):const topmostReportID = Navigation.getCurrentlyViewedReportID();
In
AuthScreensInitHandler, so the one-transaction thread lookup receives the expense report the user is actually viewing:const topmostReportID = useRootNavigationState(Navigation.getCurrentlyViewedReportID);
In
shouldShowPushNotification, so the native/Airship push path stays in sync:const topmostReportID = Navigation.getCurrentlyViewedReportID();
With
topmostReportIDnow resolving to the expense report,useOneTransactionThreadReportIDreturns the transaction thread'sreportID, the one-transaction-thread guard matches the incoming notification, and it is correctly suppressed.What specific scenarios should we cover in automated tests to prevent this bug from reoccurring?
getCurrentlyViewedReportIDwith mocked navigation states: an expense report open in the super-wide RHP over a central-pane chat returns the expense report'sreportID(not the chat's); a wideSEARCH_REPORTRHP returns itsreportID; a focusedSCREENS.SEARCH.MONEY_REQUEST_REPORTin the Search tab returns itsreportID; a plain central-paneSCREENS.REPORTkeeps the existing behavior.shouldShowReportActionNotificationreturnsfalsefor a transaction-thread notification when the associated one-transaction expense report is currently viewed (visible + focused) in each of those navigation contexts.- It still returns
truewhen the same report is not the currently viewed report, or the window is not focused, confirming we didn't over-suppress.
What alternative solutions did you explore? (Optional)
- Extend
getTopmostReportParamsitself to also look at the RHP and search screens, so every consumer ofgetTopmostReportIdtransparently benefits. I ruled this out as the primary approach becausegetTopmostReportIdis consumed by ~24 navigation-related call sites (LHN highlighting, back navigation, unread handling), and broadening its meaning risks subtle regressions elsewhere; scoping the change to the notification path is safer. - Fixing
useOneTransactionThreadReportID'suseOnyxselector (it closes overreport/chatReportwithout passing them as thedependenciesargument, so the cached result can go stale). That is worthwhile hardening for the central-pane flow and could be included in the PR, but it cannot fix this issue on its own: whenever the report is viewed in the RHP or the Reports tab, the hook's inputtopmostReportIDis already the wrong report (the parent chat orundefined), so its output can never be the right thread ID no matter how fresh the selector is.
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
Proposal
Please re-state the problem that we are trying to solve in this issue.
When User A has a single-expense report open and focused, and User B sends a message in that expense's transaction thread, User A still receives a notification — even though they are already viewing the report whose thread received the message.
What is the root cause of that problem?
This is a regression introduced by #95318 (merged 2026-07-08), which moved the local-notification path off of
Onyx.connect(). Previously,shouldShowReportActionNotificationread the topmost report and its actions from module-levelconnectWithoutViewsubscriptions inReport/index.ts(whereallReportActionswas keyed by barereportID, old line 474) and computedgetOneTransactionThreadReportIDinline with always-fresh data.#95318 replaced that with a new hook whose result is cached in a ref (
AuthScreensInitHandler.tsx:93-99) and handed to the Pusher callback (User.ts:979→shouldShowReportActionNotification). The hook readsreportandchatReportfrom two separateuseOnyxsubscriptions, then consumes them inside theREPORT_ACTIONSselector (src/hooks/useOneTransactionThreadReportID.tsx:10-14):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), });
But Onyx memoizes a
useOnyxselector against its own key's data only. Inreact-native-onyx/dist/useOnyx.js:60-78the memoized selector recomputes only whenlastInput !== input(the actions changed) or thedependenciesarray changed. Nodependenciesarray is passed here, so whenreport/chatReportarrive after the selector has cached its result, the cached value goes stale — and becausereportis stillundefinedat that moment,getOneTransactionThreadReportActionearly-returns at thereport?.typeguard (ReportActionsUtils.ts:1898), so the cached value isundefined. The ref then holdsundefined, thereportID === topmostOneTransactionThreadReportIDcheck inReport/index.ts:4616fails, and the notification fires.(The Airship push path in
shouldShowPushNotification.ts:59-64computestopmostOneTransactionThreadReportIDinline fromconnectWithoutViewdata, so it is unaffected — the broken path is the local one that fires when the tab is online and focused.)What changes do you think we should make in order to solve the problem?
Pass the external closure values as the third
dependenciesargument — the exact mechanism Onyx provides for selectors that depend on other keys (useOnyx.js:116-134invalidates the cache and forces a recompute whendependencieschange; signatureuseOnyx(key, options, dependencies?)atuseOnyx.d.ts:25). This matches the existing codebase idiom (usePolicyForTransaction.ts:42,useSelectionModeReportActions.ts:69,usePolicyData/index.ts:47):const [oneTransactionThreadReportID] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {selector: (actions) => getOneTransactionThreadReportID(report, chatReport, actions, isOffline)}, [report, chatReport, isOffline], );
That single change makes the selector recompute whenever
report,chatReport, or connectivity changes, so the cachedtopmostOneTransactionThreadReportIDalways reflects the report User A is viewing andshouldShowReportActionNotificationcorrectly suppresses the notification.What alternative solutions did you explore? (Optional)
Computing
getOneTransactionThreadReportID(...)viauseMemoover the threeuseOnyxresults instead of a selector. Also correct, but it discards the selector's re-render optimization (the reason #95318 introduced the hook) and re-rendersAuthScreensInitHandleron everyREPORT_ACTIONSupdate. Thedependenciesargument preserves #95318's intent with a one-line, idiomatic fix.🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
Triggered auto assignment to Contributor-plus team member for initial proposal review - @DylanDylann (
External)- changed the title
[-]Notification appears when user is focused on the report[/-][+][$250] Notification appears when user is focused on the report[/+]on Jul 9, 2026 74 remaining items
BugZero Checklist:
-
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: I don't think this is a regression from a previous PR. It's an edge case caused by mixing some changes from the navigation refactor and the report view implementation
Link to the comment on the Issue: -
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion:
-
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
-
[BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.
Link to issue: https://github.com/Expensify/Expensify/issues/670283
Regression Test Proposal
Test:
Precondition: User A has a one-single expense report
- Open the one-single expense report as User A
- As User B, open the transaction thread (you can invite the user by mentioning them on the report)
- As User B, send a message on the transaction thread
- While the report is open as User A, verify no notification appears
Do we agree 👍 or 👎
Zapier Logs
Run ID: 00040eee-66d4-add8-0128-00c2cfb1ff54-
- addedDailyKSv2KSv2Awaiting PaymentAuto-added when associated PR is deployed to productionAuto-added when associated PR is deployed to productionand removedWeeklyKSv2KSv2
on Aug 7, 2026 Triggered auto assignment to @mallenexpensify (
Awaiting Payment)Payment Summary
Resolving PRs:
-
fix: Notification appears when user is focused on the report #96891
-
Reviewer: @DylanDylann owed $250 via NewDot
-
Reviewer: @nkdengineer owed $250 via NewDot
BugZero Checklist (@mallenexpensify)
- I have confirmed assignees, roles, and Upwork contracts look correct
- I have paid out Upwork contracts / manual NewDot requests
-
@mallenexpensify Whoops! This issue is 2 days overdue. Let's get this updated quick!
Payment Summary
Contributor: @nkdengineer due $250 via NewDot
Contributor+: @DylanDylann due $250 via NewDot^ Test case created. Thx
$250 approved for @nkdengineer in Report ID: R00d83bEd3e3.
- Amount requested: $250.00 (Matches the payment summary here: [Due for payment 2026-08-08] [$250] Notification appears when user is focused on the report #95711 (comment).)
- I, @twisterdotcom am not already the BZ assignee. I have approved this payment.
Zapier Logs
Run ID: 00040eee-410e-a2a5-9bf5-ba28da6b07a4SO: https://stackoverflowteams.com/c/expensify/questions/7582
Approved $250 to @DylanDylann
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Version Number: 9.4.32-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #95318
Email or phone of affected tester (no customers): gocemate+buzz1833@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: windows 10/chrome
App Component: Notifications
Action Performed:
Precondition: User A has a one-single expense report
Steps:
NOTE: if you open both users on the same device (even with different tab/browser), on step 4, make sure the User A tab is open and focused (you can click into the app to make it focus)
Expected Result:
No notification should appear since user is focused on the report
Actual Result:
Notification appears when user is focused on the report
Workaround:
Unknown
Platforms:
Screenshots/Videos
Bug7203782_1783601052932.Recording__1076.mp4
View all open jobs on GitHub
Upwork Automation - Do Not Edit
Issue Owner
Current Issue Owner: @mallenexpensify