Skip to content

[Due for payment 2026-09-17] [Exploratory] Notifications-User is navigated to the Home page after opening notification and app is killed #98520

Closed

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.53-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: https://test-management.browserstack.com/projects/2219752/folder/13176925/test-cases/41237591
Email or phone of affected tester (no customers): N/A
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: iPhone 15 iOS 26.5.2
App Component: Notifications

Action Performed:

  1. As User A on a main testing device, kill the app
  2. As User B on the secondary device, send a message in 1:1 chat with User A
  3. As User A, tap the push-notification

Expected Result:

User is navigated to the correct chat after tapping the notification

Actual Result:

User is navigated to the Home page instead of the correct chat after opening a push notification while the app is killed

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7232035_1786563185924.Notification.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Mita Rachim
Bug ID: 7232035

Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. applause-bot commented on Aug 13, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/f123b7c2f7c2f7da29264dfa85c198f9dd18590387867a8284680f58e79e3eff

  2. added
    BugSomething is broken. Auto assigns a BugZero manager.
    and removed on Sep 8, 2026
  3. MelvinBot commented on Sep 8, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    navigateToReport cancels the navigation before it ever runs, because it decides "a transition is already in flight" from a disk-persisted Onyx flag rather than from the app's actual state:

    src/libs/Notification/PushNotification/subscribeToPushNotifications.ts:129

    // When transitioning to the new experience via the singleNewDotEntry flow, the navigation
    // is handled elsewhere. So we cancel here to prevent double navigation.
    if (isSingleNewDotEntry) {
        Log.info('[PushNotification] Not navigating because this is a singleNewDotEntry flow', false, {reportID});
        return;
    }

    isSingleNewDotEntry is cached from ONYXKEYS.HYBRID_APP at subscribeToPushNotifications.ts:51-61. That key is durable, and nothing can ever turn the flag back off from the App side:

    • It is not in initialKeyStates or ramOnlyKeys, so it is written to disk and is not reset on boot — src/setup/index.ts:43-92.
    • It is in KEYS_TO_PRESERVE, so it survives Onyx.clear() on sign-out — src/libs/actions/App.ts:155.
    • The native handoff payload is applied with Onyx.METHOD.MERGE, so a payload that simply omits isSingleNewDotEntry cannot clear a stale true — src/libs/actions/Session/index.ts:807-813.
    • No non-test code in App ever writes the flag to false.

    So once the flag is true — either because it went stale, or because the native side legitimately sets it for a notification launch — every push tap on that install is silently dropped. Nothing navigates, so the user is left wherever the cold boot put them, which is Home. There is no error, only an Log.info line.

    This is a regression in how the guard is written. It started out route-scoped, which made it safe:

    // fd591aa9ff0, 2025-02-28 — "fix double navigation"
    if (Navigation.getActiveRoute().includes(CONST.HYBRID_APP.SINGLE_NEW_DOT_ENTRY)) {

    11f879cb19d ("post-merge fixes", 2025-03-04) swapped that for the bare flag read, and CONST.HYBRID_APP.SINGLE_NEW_DOT_ENTRY no longer exists anywhere in the repo. Every other consumer of this flag still pairs it with a route check — for example src/components/ScreenWrapper/index.tsx:207 ANDs it with initialURLMatchesActiveRoute. The push-notification guard is the only one that lost that safety.

    ⚠️ Confidence: medium. The mechanism is code-verified end to end, but I could not confirm it is what fired here. I had no production logs available in this run, and the value the native side puts in the handoff payload on a killed-app notification launch is decided outside this repo, so I can't verify it. See alternatives below.

    Why the timing/race theories don't hold, and why iOS only
    • Airship listener bound too late. PushNotification.init() attaches the Airship listener at line 33 before onSelected populates the handler map at lines 36-43, which looks like a cold-start race. It isn't reachable: the buffered launch event replay round-trips through the native bridge and awaits takePendingEvents, while lines 36-43 run synchronously in the same tick. Ruled out.
    • waitForProtectedRoutes() hanging. This is exactly what caused the Android sibling issue, but that code is unchanged since the 2026-05-28 revert and is not gated on TabNavigator in this build. Ruled out.
    • iOS only. On Android, PushReceived can run via Headless JS while the app is killed, so report data is already in Onyx by the time the tap is handled. iOS has no such path — nothing runs until the full RN app boots, so the tap navigation is entirely dependent on this one code path. That asymmetry is consistent with the report being iOS-only, though it does not prove the guard is at fault.

    What changes do you think we should make in order to solve the problem?

    Stop treating a persisted flag as evidence of an in-flight transition. In navigateToReport, only skip navigation when the app is actually sitting on the single-entry transition route, mirroring what ScreenWrapper already does — pair isSingleNewDotEntry with a current-route or initialURL check instead of reading it raw.

    Two things worth doing alongside it:

    1. Add a fallback. The guard's premise is that "the navigation is handled elsewhere." On a push tap there is no evidence anything else has the target reportID, so when the guard skips, the notification is lost with no recovery. If the guard stays, it needs a path that still reaches the report.
    2. Move the transient flags off a durable key. src/types/onyx/HybridApp.ts:26-65 holds isSingleNewDotEntry, closingReactNativeApp, readyToShowAuthScreens, signingInWithSAML and transitionStartTimestamp — all in-flight state — on a key that is disk-persisted and preserved across Onyx.clear(). App already has a RAM_ONLY_* convention for exactly this (src/setup/index.ts:70-92).

    Two cheap things that would confirm this before anyone writes code:

    • Search production logs for [PushNotification] Not navigating because this is a singleNewDotEntry flow on iOS cold starts. Its presence confirms this root cause outright and sizes the impact. If it's absent, check for [PushNotification] onSelected() - Navigation is ready. Navigating... to see whether navigation was attempted at all.
    • Confirm from the BrowserStack test case whether this was the HybridApp build or the standalone NewDot build. On standalone, CONFIG.IS_HYBRID_APP is false, HYBRID_APP is never populated, and this theory is void.

    Note on scope: the guard itself is in App and fixable here. But whether the flag is stale or is legitimately set on a notification launch is decided by the native OldDot side, which lives outside this repo. If the log line above confirms the flag is firing, an internal engineer with access to that repository should confirm which of the two it is, since that determines whether the App-side fix is sufficient or the handoff payload also needs correcting.

    What alternative solutions did you explore? (Optional)

    Two other candidates, both unresolved

    Airship iOS SDK bump. Expensify/App#92497 (merged 2026-06-08, present in 9.4.53-0) took the Airship iOS native SDK from 19.8.2 to 20.7.0 — the SDK that owns cold-start notification buffering and replay. On the App side it touched only Podfile, Podfile.lock and project.pbxproj, with zero Swift changes, so any behavioral change is inside the SDK or its companion native PR, neither of which I can read. Its QA steps only covered a warm tap, never a killed-app tap. If the log line above is absent, this becomes the leading candidate and needs an internal engineer.

    Empty reportID in the payload. src/ROUTES.ts:2506-2509 — REPORT_WITH_ID.getRoute() returns ROUTES.HOME on a falsy reportID, so an empty-string reportID would navigate the user to Home explicitly. This is a literal "lands on Home" path, but it needs the sender to emit '' (the type declares reportID: number), and it would log Invalid reportID is used to build the REPORT_WITH_ID route. Worth grepping for alongside the line above; I could not verify the sender.


    Related issues

    • Expensify/App#92138 — "Android: Tapping push notification on cold start opens last active chat instead of the notification's chat" (closed). Same class of bug: a cold-start push tap that never reaches the target chat. It was closed on 2026-06-01 as a regression from a PR that got reverted — no forward fix was ever merged, and the only proposed fix PR was closed unmerged. Given that, this issue may be worth treating as a continuation rather than a new bug. The differing destination ("last active chat" vs "Home") is explained by Expensify/App#97446 (2026-07-30) changing what the root path resolves to.
    • Expensify/App#99732 — "Push notification is received, but the message is not shown in chat" (open). Possibly related; same subsystem, different symptom.

    Process note: this area has had three reverts in ten weeks. Any fix here should include an explicit killed-app tap in its QA steps — the warm tap works and is what has been getting tested.

    I could not verify this on a device: the forced test platform for this run is web, and the web PushNotification module is a no-op stub, so a killed-app iOS push tap is not reproducible there.


    💡 Suggested AI code review rule

    This looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for Expensify/App:

    • Rule: CONSISTENCY-18 — Transient state belongs in a RAM-only Onyx key
    • Condition: Flag a boolean or timestamp field that represents an in-flight operation (isSingleNewDotEntry, closingReactNativeApp, signingIn*, readyToShow*, transition*Timestamp) when it lives on a disk-persisted Onyx key. Either move it to a RAM_ONLY_* key, or require every consumer to AND it with a current-route or initialURL check.
    • Why: A persisted flag outlives the operation it describes. Onyx.merge cannot clear an omitted key, and KEYS_TO_PRESERVE carries it through sign-out, so one stale true silently disables a feature forever with no error — the mechanism behind this issue.
    • Search pattern: fields in src/types/onyx/*.ts matching /^\s*(is|should|has|closing|signingIn|readyTo)[A-Za-z]*\??:\s*boolean/ whose key appears in KEYS_TO_PRESERVE but not in ramOnlyKeys; then check each consumer for an accompanying route/URL guard.
    • Offending code (❌): subscribeToPushNotifications.ts:129
    • Correct form (✅): ScreenWrapper/index.tsx:207

    The full rule file, with the ❌/✅ code inline in App's coding-standards format, would go in Expensify/App itself. If it's worth adding, a maintainer can open it there as a draft PR.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/98520) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 8, 2026
  5. melvin-bot commented on Sep 8, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @abdulrahuman5196 (External)

  6. melvin-bot commented on Sep 8, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  7. 9 remaining items

  8. changed the title [-][Exploratory] Notifications-User is navigated to the Home page after opening notification and app is killed[/-] [+][Due for payment 2026-09-17] [Exploratory] Notifications-User is navigated to the Home page after opening notification and app is killed[/+] on Sep 10, 2026
  9. melvin-bot commented on Sep 10, 2026

    @melvin-bot

    @abdulrahuman5196

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.4.75-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-09-17. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  10. melvin-bot commented on Sep 14, 2026

    @melvin-bot

    @abdulrahuman5196 Whoops! This issue is 2 days overdue. Let's get this updated quick!

  11. melvin-bot commented on Sep 16, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  12. melvin-bot commented on Sep 16, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  13. melvin-bot commented on Sep 21, 2026

    @melvin-bot

    @mallenexpensify Eep! 4 days overdue now. Issues have feelings too...

  14. mallenexpensify commented on Sep 22, 2026

    @mallenexpensify
    Contributor

    Closing. @abdulrahuman5196 doesn't look like you're due payment here, comment and tag me if that's not correct. Thx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions