Skip to content

Pass NVPs to last accessed report calls - #98719

Merged
blimpich merged 35 commits into
Expensify:mainfrom
ShridharGoel:refactor/66422-last-accessed-report-nvp
Sep 22, 2026
Merged

blimpich merged 35 commits into
Expensify:mainfrom
ShridharGoel:refactor/66422-last-accessed-report-nvp

Conversation

@ShridharGoel

@ShridharGoel ShridharGoel commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Pass report name-value pairs to every findLastAccessedReport() caller. This lets deep-link, room-leave, group-chat-leave, and task-delete fallback navigation exclude archived reports without reading the collection inside ReportUtils.

Fixed Issues

$ #66422
PROPOSAL:

Tests

  1. Sign in to an account.
  2. Create two reports.
  3. Delete one of the reports.
  4. Open a link to that deleted report.
  5. Verify the "Hmm... it's not here" page appears. This is the existing behavior for signed-in users.
  6. Leave a root group chat and verify the app opens the active recent chat/report instead of the archived report.
  7. Leave a workspace room and verify the details modal closes while the room stays open with a Join button. This is the existing workspace-room behavior.
  • Verify that no errors appear in the JS console

Offline tests

QA Steps

Same as tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-08-16.at.12.17.54.PM.mov
Screen.Recording.2026-08-16.at.12.07.46.PM.mov

@codecov

codecov Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/DeepLinkHandler.tsx 95.31% <100.00%> (+8.64%) ⬆️
src/hooks/useFindLastAccessedReport.ts 100.00% <100.00%> (ø)
.../AppNavigator/Navigators/ReportsSplitNavigator.tsx 95.00% <100.00%> (ø)
src/libs/actions/Report/index.ts 71.35% <100.00%> (-0.73%) ⬇️
src/libs/actions/TaskDeletion.ts 81.63% <100.00%> (ø)
src/libs/navigateAfterOnboarding.ts 88.46% <100.00%> (+0.96%) ⬆️
src/pages/inbox/ReportRouteParamHandler.tsx 72.22% <100.00%> (-6.04%) ⬇️
src/libs/actions/Link.ts 58.30% <0.00%> (ø)
src/pages/DynamicReportDetailsPage.tsx 81.94% <50.00%> (-0.18%) ⬇️
src/libs/ReportUtils.ts 87.38% <80.00%> (-0.02%) ⬇️
... and 19 files with indirect coverage changes

@ShridharGoel
ShridharGoel marked this pull request as ready for review August 16, 2026 06:50
@ShridharGoel
ShridharGoel requested review from a team as code owners August 16, 2026 06:50
@melvin-bot
melvin-bot Bot requested review from DylanDylann and flaviadefaria and removed request for a team and flaviadefaria August 16, 2026 06:50
@melvin-bot

melvin-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44d2d8f387

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/DeepLinkHandler.tsx Outdated
@DylanDylann

Copy link
Copy Markdown
Contributor

@ShridharGoel The findLastAccessedReport function uses allReport and allReportNameValuePair to compute the output. I noticed that across all usages of this function, we only use the reportID and chatType fields. Since the computation looks heavy, I'd suggest converting this function into a hook (using a selector internally) instead of subscribing to and passing large collections into the function

cc @tgolen @blimpich

@tgolen

tgolen commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

@DylanDylann I agree with your approach. Thanks for suggesting it!

Callers only consume reportID, policyID, and chatType, so the
function now returns a minimal LastAccessedReport slice instead
of the full Report. Extracts a pure findLastAccessedReportSelector
that takes the collections as arguments; the module-scoped
findLastAccessedReport wrapper keeps feeding it the intentional
module subscriptions (non-UI, no view-based re-renders).
navigateAfterOnboarding drops isConciergeChatReport/isSelfDM
calls and compares slice fields directly.

Refs Expensify#98719 (comment)
Resolve conflicts in DeepLinkHandler.tsx and ReportUtils.ts:
- DeepLinkHandler: keep PR's reportNameValuePairs loading guard + arg,
  adopt main's guidedSetupAndTourStatus refactor (replaces standalone
  isSelfTourViewed).
- ReportUtils: keep the pure findLastAccessedReportSelector + slice
  return type from this PR, and fold main's new `reports` param into
  the module-scoped findLastAccessedReport wrapper so callers like
  ReportRouteParamHandler can pass a subscribed reports collection.
Drop verbose rationale and fix the stale "only takes the name-value
pairs" line, since the wrapper now also accepts a `reports` argument.
findLastAccessedReportSelector and FindLastAccessedReportOptions have
no external consumers; tests exercise the public wrapper. Keep both
module-private (selector still backs the wrapper, type still types
its options param).
@ShridharGoel

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Review

The refactor is correct and the primary path is verified working (see smoke test below). Two things to resolve before this is mergeable — one design, one scope:

1. This still passes large collections — the opposite of what was agreed

DylanDylann suggested converting findLastAccessedReport into a hook (selector internally) "instead of subscribing to and passing large collections into the function", and tgolen agreed. This PR does introduce a pure findLastAccessedReportSelector and returns a minimal LastAccessedReport slice — good — but the click/deep-link callers still subscribe to the whole REPORT_NAME_VALUE_PAIRS collection and thread it through (DeepLinkHandler, DynamicReportDetailsPage), which is the pattern the reviewers explicitly steered away from. Please confirm with DylanDylann that this middle-ground satisfies the suggestion, or wrap it in a hook as originally requested.

2. The Onyx.connect() this issue targets is not actually removed

The issue's deliverable is to remove the Onyx.connect() for REPORT_NAME_VALUE_PAIRS and decrement the ESLint warning count. That Onyx.connect() is still present because allReportNameValuePair is used by two other functions in the file (ReportUtils.ts:12312, ReportUtils.ts:14218). That's fine if this is an intentional incremental step, but please note in the PR body that the connect removal + ESLint decrement is deferred to a follow-up, so it isn't mistaken for the full fix.

Correctness — checked and looks good ✅
  • Inlined helpers in getReportIDAfterOnboarding are equivalent. navigateAfterOnboarding.ts:64-68 replaces isConciergeChatReport(r, id) with r.reportID !== conciergeReportID and isSelfDM(r) with r.chatType !== SELF_DM. Both match the originals (isConciergeChatReport = reportID === conciergeReportID, isSelfDM = getChatType() === SELF_DM, and getChatType just returns report?.chatType). The falsy-conciergeReportID edge case also lines up since LastAccessedReport.reportID is always defined.
  • The minimal-slice return is safe. Every caller (Link, Report/index, ReportsSplitNavigator, ReportRouteParamHandler, navigateAfterOnboarding) reads only reportID, policyID, or chatType, all of which LastAccessedReport carries.
  • DeepLinkHandler ref pattern is right. reportNameValuePairsRef keeps the URL-change listener reading the latest NVPs without re-registering on every collection change, and the new test in DeepLinkHandlerTest.tsx covers exactly that (sets NVPs after mount, fires a url change, asserts the latest value is passed).
Smoke test (web) — initial navigation still works ✅

findLastAccessedReport runs on app load (via ReportsSplitNavigator) to pick the initial report, so I verified that path didn't regress: the app loaded to Home/Inbox without a crash or error page, the LHN rendered, and opening a report worked normally. I did not exercise the archived-report fallback edge cases (deleted-report deep link, leaving a group chat/room) since they need specific data setup — those still warrant manual QA per your Tests section.

Home tab after initial load
Home tab after initial load

Inbox LHN
Inbox LHN with Concierge report

Report opened from LHN
Concierge report opened from LHN


view run

@ShridharGoel

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Re-review

Thanks for the updates — the main design point from my last review is addressed: you introduced useFindLastAccessedReport (hook wrapping a useOnyx selector) and adopted it in ReportRouteParamHandler, which is the direction DylanDylann and tgolen asked for. This looks good and the initial-navigation path still works (smoke test below).

Two things left to sort out before merge:

1. DynamicReportDetailsPage still subscribes to the full NVP collection

The imperative click handlers (leaveRoom / leaveGroupChat / deleteTask) still receive the whole REPORT_NAME_VALUE_PAIRS collection via lastAccessedReportNameValuePairs. That's the "pass a large collection" pattern the reviewers wanted to avoid — but action functions can't call a hook, so some passing is unavoidable here. Worth a quick confirm with DylanDylann whether threading the collection into these action fns is acceptable, or whether they'd prefer you precompute the fallback report ID at render time (via the new hook, with excludeReportID = report.reportID) and pass just that ID. Same question applies to the DeepLinkHandler ref path, which by nature is imperative/async and reasonably keeps passing the collection.

2. The Onyx.connect() this issue targets still isn't removed

ReportUtils.ts:1211 Onyx.connect() for REPORT_NAME_VALUE_PAIRS remains, because allReportNameValuePair is still read by two other functions (:12312, :14218). That's fine as an incremental step, but please call out in the PR body that the connect removal + ESLint-count decrement (step 5 of the issue) is deferred to a follow-up, so it isn't read as the complete fix.

Correctness — checked, looks good ✅
  • The hook's selector correctly reacts to NVP changes. The selector closes over reportNameValuePairs (a different Onyx key than the subscribed REPORT collection), which is normally a stale-closure risk. It's safe here because the inline arrow is a new reference every render, so useOnyx's useMemo([selector]) rebuilds the memoized selector each render and re-runs it — and the separate useOnyx(REPORT_NAME_VALUE_PAIRS) subscription re-renders the component whenever NVPs change. So the fallback is always computed against current NVPs. ReportRouteParamHandler also correctly gates on both reportNameValuePairsMetadata and reportsMetadata before acting.
  • enabled: shouldResolveReportID neatly skips the O(n) scan when a reportID is already present.
  • Prior findings still hold: the inlined isConciergeChatReport/isSelfDM equivalents in navigateAfterOnboarding, the minimal LastAccessedReport return slice (all callers read only reportID/policyID/chatType), and the DeepLinkHandler ref pattern all remain correct and test-covered.
Smoke test (web) — initial navigation still works ✅

findLastAccessedReport runs on app load to pick the initial report, so I re-verified that path: the app loaded to Home/Inbox without a crash or error page, the last-accessed report auto-opened, and opening/navigating a report from the LHN worked normally. I did not exercise the archived-report fallback edge cases (deleted-report deep link, leaving a group chat/room) since they need specific data setup — those still warrant manual QA per your Tests section. I couldn't run the unit tests locally in this environment, but CI covers them.

Inbox — last-accessed report auto-opened after load
Inbox with last-accessed report auto-opened after load

Report view intact after navigating away and back
Report view intact after navigating away and back


view run

@ShridharGoel

Copy link
Copy Markdown
Contributor Author

@DylanDylann Any updates on the final review?

Comment thread src/hooks/useFindLastAccessedReport.ts Outdated
};

/** Resolves the last accessed report for navigation fallbacks from a view-scoped subscription. */
function useFindLastAccessedReport({openOnAdminRoom, excludeReportID, enabled = true}: UseFindLastAccessedReportOptions = {}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function useFindLastAccessedReport({openOnAdminRoom, excludeReportID, enabled = true}: UseFindLastAccessedReportOptions = {}) {
function useFindLastAccessedReport({openOnAdminRoom, enabled = true}: UseFindLastAccessedReportOptions = {}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's redundant now

const shouldResolveReportID = !routeReportID && !reportIdFromPath && !isTransitioning;

const {lastAccessedReportID} = useFindLastAccessedReport({
openOnAdminRoom: isOpenOnAdminRoom,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
openOnAdminRoom: isOpenOnAdminRoom,

@DylanDylann

Copy link
Copy Markdown
Contributor

@ShridharGoel last NITs

@ShridharGoel

Copy link
Copy Markdown
Contributor Author

@DylanDylann Updated

@melvin-bot
melvin-bot Bot requested a review from blimpich September 22, 2026 07:32
blimpich
blimpich previously approved these changes Sep 22, 2026
Comment thread src/hooks/useFindLastAccessedReport.ts Outdated
@blimpich

Copy link
Copy Markdown
Contributor

Merge conflicts 😢. @ShridharGoel can you fix those, and while you're at it can you change the comments to use // instead of /** */?

…ccessed-report-nvp

# Conflicts:
#	src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx
#	src/pages/DynamicReportDetailsPage.tsx
@blimpich
blimpich merged commit 5ee2883 into Expensify:main Sep 22, 2026
52 of 73 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚧 blimpich has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/blimpich in version: 9.4.92-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 No help site changes are required for this PR, so I did not create a draft docs PR.

This is an internal refactor with no user-facing behavior change. It threads the reportNameValuePairs collection through findLastAccessedReport() callers so the function stops reading the module-scoped Onyx.connect() copy — part of Remove Onyx.connect() for the key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS in src/libs/ReportUtils.ts.

Why nothing in docs/articles needs updating

What changed: parameter plumbing and a return-type narrowing, not product behavior.

No user-visible surface moved:

  • No new or renamed feature, tab, setting, button, or menu item.
  • No copy changes — src/languages/* is untouched.
  • The PR's own test steps confirm the outcomes are unchanged: opening a link to a deleted report still shows the "Hmm... it's not here" page, and leaving a workspace room still closes the details modal and leaves a Join button. Both are called out as existing behavior.

No matching article exists: the help site documents how to leave a chat room (Chat Features and Moderation Tools → Leave a Chat Room: open the chat room, click the chat room header, select Leave). It does not document which report the app lands on afterward, and it has no article covering deep links to deleted reports or last-accessed-report fallback. Adding that now would be new documentation of internal navigation behavior, not an update this PR makes necessary.

Since no docs PR exists, the HelpDot label, the assignment, and the "review the linked help site PR" follow-up don't apply here. If you'd rather document the leave-a-chat landing behavior as a new addition, say the word and I'll draft it.

⚠️ I could not verify this in a browser — web prep failed in this run, so NewDot couldn't be driven. The assessment above is from the diff, the linked issue, and the help site markdown.


view run

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/lakchote in version: 9.4.92-8 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants