Skip to content

Navigate to the clicked expense in a multi-expense report preview (v2) - #97308

Merged
MonilBhavsar merged 51 commits into
Expensify:mainfrom
wildan-m:wildan/91042-multi-expense-preview-per-item-nav-v2
Sep 7, 2026
Merged

MonilBhavsar merged 51 commits into
Expensify:mainfrom
wildan-m:wildan/91042-multi-expense-preview-per-item-nav-v2

Conversation

@wildan-m

@wildan-m wildan-m commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

v2 of #92546, which was reverted in #97198 after five deploy blockers.

Tapping a card in the multi-expense Report Preview carousel opens that expense instead of the parent report.

  • Wide layouts — unchanged from v1: the report opens in the super-wide RHP and the pressed expense cascades on top (180ms stagger).
  • Narrow layouts — the report opens first and the pressed expense cascades on top of it in the RHP, so back returns to the report and back again to the chat. Same order as wide, and the same order users had before the revert.

What caused the blockers, and what v2 does about it

v1 inserted the parent report beneath the expense by pushing the thread into the split navigator — a spliced stack entry on native (Navigation.openExpenseOverParentReport) and an extra history entry on mobile web. Every native blocker traced back to that hand-built stack being mutated by another flow:

blocker what went wrong
#97184 delete from details → goBack couldn't param-match the spliced parent (its backTo isn't in routeParamsIgnore) → REPLACE inserted a duplicate report route → back showed the report twice
#97183 split-expense save → removeScreenByKey only filters the root navigator, so the stale thread route nested in the split survived → back landed on the LHN
#97158 resolve duplicates → deleting the open thread while the spliced parent sat beneath it crashed on Android
#97149 an expense deleted while offline stays in the carousel but its thread is gone → "It's not here"

v2 removes the splice entirely. Navigation.ts is untouched by this PR — no custom stack manipulation, nothing for those flows to trip over. The back order is preserved by opening the report as an ordinary split screen and keeping only the expense in the RHP, so the thread never becomes a nested split-navigator screen. #97149 is additionally guarded explicitly: a pending-DELETE press opens the parent report, and offline-deleted expenses are kept out of the expense view's prev/next list.

The expense view's prev/next arrows are also seeded with every expense on the report, in the order the carousel sorts them (RBR first, then date) rather than raw collection order, so on a report where those differ the arrows no longer walk a sequence that isn't on screen, and on a report with more than 11 expenses they still reach the ones the carousel does not render.

Note on #97155**.** It was closed with the revert, and its expected result was "only the specific expense should open". This PR opens the report underneath and cascades the expense on top, on both layouts — that is what preserves the pre-revert back order. Flagging it explicitly so it can be confirmed rather than assumed.

Fixed Issues

$ #91042
$ #97149
$ #97158
$ #97183
$ #97184
PROPOSAL: #91042 (comment)

The four deploy blockers above were closed by the revert in #97198 rather than fixed. They are linked here because they were caused by the reverted PR and would return on a re-land, so this PR has to address each one; the QA steps below cover them individually. #97155 is deliberately not listed — see the note in the explanation above.

Tests

  1. Open a workspace chat containing a report preview with two or more expenses.
  2. Tap one of the expense cards in the carousel.
  3. Verify the expense you tapped opens (not the parent report), on both wide and narrow.
  4. Press back. Verify you return to the report, and back again returns to the chat.
  5. Repeat for a different card and verify it opens that card's expense.
  6. Open a report preview with exactly one expense, tap it, and verify the report opens.
  7. Verify the View button still opens the report.
  8. On a report where an expense is back-dated (so display order differs from the order the expenses were created), open the first card shown and verify the prev arrow is disabled and the next arrow steps through every expense in the order the report shows them, including any beyond the 11 cards the carousel renders.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline.
  2. In a multi-expense report preview, delete one of the expenses.
  3. The deleted expense stays visible in the carousel — tap it.
  4. Verify the parent report opens and you do not land on "It's not here" ([Due for payment 2026-09-16] Report - Expense deleted offline can still be opened and leads to "It´s not here" RHP. #97149).
  5. Open a different expense in the same report and verify the deleted one is not reachable through the prev/next arrows.

QA Steps

Same as Tests, plus the regression checks from the reverted PR's blockers:

  1. Android - Report -After deleting expense from details page, back button leads to report again #97184 — Android: tap an expense preview → header → details → delete the expense. Verify you land on the parent report and one back reaches the chat.
  2. Android - Expense - Back button returns to LHN after splitting expense #97183 — Android: tap an expense preview → More > Split > Save, then hardware back. Verify you return to the workspace chat, not the LHN.
  3. [Due for payment 2026-09-16] Android - Report - App crashes after keeping one duplicate expense. #97158 — Android: workspace chat with two similar expenses → tap the second preview → Review Duplicates → select the other expense → Keep Selected → Confirm. Verify no crash.
  • 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
Kapture.2026-08-11.at.22.46.50.mp4
Android: mWeb Chrome
Kapture.2026-08-11.at.22.56.20.mp4
iOS: Native
Kapture.2026-08-11.at.21.53.09.mp4
iOS: mWeb Safari
Kapture.2026-08-11.at.22.36.16.mp4
MacOS: Chrome / Safari
Kapture.2026-08-11.at.21.39.06.mp4

Tapping a card in the multi-expense report preview carousel opens that expense instead of the parent report. Wide layouts keep the report in the super-wide RHP and cascade the pressed expense on top; narrow layouts open the expense with a single forward navigation, so back returns to the chat.

This is v2 of Expensify#92546, which was reverted for five deploy blockers. v1 inserted the parent report beneath the expense so back stopped on the report first -- a spliced stack entry on native, an extra history entry on mobile web. That hand-built stack is gone: it broke whenever another flow mutated it (Expensify#97184 replaced the deleted thread with a duplicate report route, Expensify#97183 left a stale route after a split save, Expensify#97158 crashed resolving duplicates) and on mobile web the extra entry forced a state rebuild that flashed the chat on back. Navigation.ts is untouched by this version.

Also guards an expense deleted while offline (Expensify#97149): those rows stay in the carousel but their thread is gone, so pressing one now opens the parent report instead of landing on 'It's not here'.
@codecov

codecov Bot commented Aug 4, 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 Δ
...ReportPreview/MoneyRequestReportPreviewContent.tsx 100.00% <ø> (ø)
...eportPreview/MoneyRequestReportPreviewProvider.tsx 94.52% <100.00%> (+3.21%) ⬆️
...RequestReportPreview/ReportPreviewActionButton.tsx 100.00% <100.00%> (ø)
...em/MoneyRequestReportPreview/resolvePressOrigin.ts 100.00% <100.00%> (ø)
...yRequestReportPreview/useReportPreviewCarousel.tsx 79.12% <100.00%> (+3.56%) ⬆️
src/hooks/useNavigateToTransactionThread.ts 81.81% <100.00%> (+34.44%) ⬆️
src/libs/actions/TransactionThreadNavigation.ts 93.75% <100.00%> (+22.78%) ⬆️
src/styles/index.ts 64.03% <ø> (ø)
...stReportView/MoneyRequestReportTransactionList.tsx 0.28% <0.00%> (-0.01%) ⬇️
...tView/MoneyRequestReportTransactionsNavigation.tsx 68.10% <73.91%> (+68.10%) ⬆️
... and 1 more
... and 18 files with indirect coverage changes

wildan-m added 11 commits August 4, 2026 23:16
…f the carousel

Narrow layouts now open the pressed expense via ROUTES.SEARCH_REPORT, the same RHP route every other narrow entry point uses, instead of pushing it as a split-navigator screen. Putting the thread in the split stack is what the cleanup flows do not expect: the split-expense save relies on removeScreenByKey, which only filters the ROOT navigator's routes and can never remove a nested split screen (Expensify#97183), and it leaves the discarded thread as a focused full-screen report while its data is torn down (Expensify#97158). The RHP leaves the split stack exactly as those flows assume.

Also stops seeding the expense view's prev/next carousel with offline-deleted rows. The press handler already opened the parent report for them (Expensify#97149), but the sibling arrows were seeded from the unfiltered list, so the deleted expense stayed reachable and still landed on 'It's not here'. Every other seeder in the tree filters with isTransactionPendingDelete; this one now does too.

Unmarks the expense's RHP width hint when the wide cascade aborts, so a hint can't leak and force an unrelated later open to be wide.
resolveChildReportID falls back to the transaction's own transactionThreadReportID when the IOU action isn't in OnyxDB (cache clear, or a chat whose expense report was never opened). v1 got away with that because the narrow path pushed the parent report first, which mounted the report screen and fetched its actions. v2 opens the expense directly, so nothing hydrated them — and the expense view's prev/next carousel resolves each sibling through those actions. With them missing the carousel could not find the sibling's existing thread and would mint a parentless duplicate thread instead, landing on an empty expense view. Fetch the actions in the background when we take that fallback; the press still opens immediately.

Also clears the seeded sibling IDs when the wide cascade aborts. The screen that clears them on unmount is never reached in that case, so they would outlive the interaction.
The suite mocked useThemeStyles as {flex1: {}}, so styles.flexRow and styles.gap2 resolved to undefined and were dropped from the style array — the two-button row that issue Expensify#91042 asks for was completely untested and a typo'd style key would have passed. Give the mock real sentinel styles and assert the row is applied when a primary action is present and NOT applied when View stands alone.
…ne seeding

Two gaps the existing suite left open:

- No test asserted that an offline delete-pending expense is still RENDERED. Issue Expensify#26939 (and OfflineWithFeedback's own rule, which hides a pending-delete item only when ONLINE) require the card to stay visible and greyed while offline; v2 only makes it non-navigable. Verified the test fails if the display filter stops honouring isOffline.

- No test pinned the new openableTransactionIDs list as an offline-only refinement. Online, a delete-pending row is already filtered upstream, so the seeded array must still equal the full visible list — this catches anyone widening the predicate and silently dropping live siblings from the prev/next carousel.
pendingExpenseTransactionRef was only ever cleared by the drain effect, so a press that deferred waiting on the report's actions stayed armed. Pressing a second card that resolved immediately opened that expense, but when the first press's fetch landed the drain fired and navigated away to the first expense instead — the user ends up on a card they pressed several seconds earlier. Clear the pending press at the top of every press so the latest one always wins.
Three confirmed races, all in the report preview's press handling:

- A deferred press was armed until another CARD press cleared it. Opening the report instead ('View', or the preview itself) left it armed, so when the report-actions fetch landed the app navigated to that expense from wherever the user had gone. The View button is wired to a separate openReportFromPreview in the provider, so clearing it in this file's handler is not enough: record the route the press was made on and drop the press if the active route has moved by the time the fetch settles.

- The wide cascade's setTimeout was never cancelled. A second press within the 180ms delay let the first press's timer open the wrong expense and then run its cleanup over the expense actually on screen. Keep a handle and clear it on any later press, on opening the report, and on unmount.

- The cascade's abort path cleared the sibling-transaction IDs unconditionally. That key is global, so an abort could wipe a carousel another flow had seeded during the delay. Only clear it if it still holds what this press wrote.

Also stops opening a thread shell that has no parent action while offline: with the IOU action absent and no way to fetch it, open the parent report instead — the same fallback the deferred path already uses.
Two behaviors the suite could not see:

- Every fallback assertion ran on a wide layout, so the narrow branch of openReportFromPreview had no coverage at all — the whole branch could be deleted and the suite stayed green. Narrow has no super-wide RHP, and this is the route a deleted-expense tap and an offline dead tap both land on.

- The reserved RHP widths never reach the rendered output, so every mark/unmark call could be removed without failing anything. Assert the report is widened, the pressed expense is widened, and that the expense's width is released when the press is abandoned.

Both were checked by deleting the code under test and confirming the new tests fail.
Restores the original back order on narrow: back from the expense returns to the report, and back again to the chat, matching how the wide layout already cascades.

The expense still opens in the RHP rather than as a screen inside the split navigator. That distinction is what the deploy blockers came from, not the back order. A thread pushed into the split stack is a full-screen report route, and the flows that clean up after a thread cannot reach it there: the split-expense save path calls removeScreenByKey, which only filters the root navigator's routes and so can never remove a nested split screen, and the delete path's goBack can land on a second copy of the report. Opening the report as an ordinary split screen and keeping only the expense in the RHP gives the same back order while leaving the split stack exactly as those flows expect it.

The cascade reuses the wide layout's timer, including the guard that drops the delayed navigation when the user has moved on and only clears the seeded sibling IDs when they still belong to this press.
The carousel sorts before rendering, putting transactions with a red brick road first and tiebreaking by date, but the prev/next arrows were seeded from the raw transaction collection. On a report where those two orders differ, the arrows walked a sequence the user could not see: pressing next on the visually last card jumped to the visually first one, and next on the first card was disabled.

The sorted list is owned by useReportPreviewCarousel, while the press handler that seeds lives above it, so the carousel now reports its rendered order upward and the seeding uses that. Sorting a second time next to the press handler was the alternative, but it would have meant duplicating the comparator and re-reading violations and the report owner's login, leaving two orderings to keep in step.

The seeded list is read once per press, since the cascade's abort compares it by identity against what is currently seeded.
…expense-preview-per-item-nav-v2

# Conflicts:
#	src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
@wildan-m
wildan-m marked this pull request as ready for review August 11, 2026 16:10
@wildan-m
wildan-m requested review from a team as code owners August 11, 2026 16:10
@melvin-bot

melvin-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

@Ollyws 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]

@melvin-bot
melvin-bot Bot removed request for a team August 11, 2026 16:10
@wildan-m

Copy link
Copy Markdown
Contributor Author

@trjExpensify, @jayeshmangwani is it possible that regression tests are conducted before merging into main? I suspect there might still be a hidden error because this change is affecting many existing flow.

@wildan-m

wildan-m commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

@huult Sorry, that reply was analysis when you asked for a recording. Android, current branch — PR flows, rapid double-press, and the split after the update:

97308-1-card-back-view-arrows

97308-1-card-back-view-arrows.mp4

97308-2-rapid-double-press

97308-2-rapid-double-press.mp4

97308-3-split-after-update

97308-3-split-after-update.mp4

…expense-preview-per-item-nav-v2

# Conflicts:
#	src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
…expense-preview-per-item-nav-v2

# Conflicts:
#	tests/ui/MoneyRequestReportPreview.test.tsx
@huult

huult commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

@wildan-m Could you test this on the iOS simulator as well? There’s an issue mentioned here: #97308 (comment). Could you do one round of testing on the simulator to make sure everything works correctly? I just retested it, and it’s still happening.

@wildan-m

wildan-m commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

@huult i can repro this in main #97308 (comment)
we only see update after click add expense, do you think that's regression from my PR?

Kapture.2026-09-05.at.09.26.11.mp4

…ort list re-seeds

The report list re-seeds the prev/next arrows whenever its visual order changes while an expense is focused on top. A press in the report preview seeds the same rows in the carousel's order (violations first), so the refresh that follows the press replaced that order with the list's own and the first card no longer had a disabled prev arrow. The list now keeps a seed that covers exactly its rows and only re-seeds when the rows themselves change.
@wildan-m

wildan-m commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

iOS simulator.

Tap card → expense → Back → report → Back → chat

01-card-tap-expense-back-report-back-chat.mp4

View → report

02-view-button-opens-report.mp4

#97184 delete → Back

03-blocker-97184-delete-from-details-then-back.mp4

#97183 split → Save → Back

04-blocker-97183-split-save-then-back.mp4

#97158 review duplicates

05-blocker-97158-review-duplicates-keep-confirm.mp4

Expense arrows, first card

06-expense-arrows-first-card-prev-disabled.mp4

Split → Save, hands off

branch-split-halves-7s-handsoff.mp4

@huult

huult commented Sep 6, 2026

Copy link
Copy Markdown
Contributor
Screen.Recording.2026-09-06.at.09.55.43.mov

@MonilBhavsar The PR is ready. All yours for final review.

@MonilBhavsar
MonilBhavsar merged commit af79668 into Expensify:main Sep 7, 2026
34 of 35 checks passed
@OSBotify

OSBotify commented Sep 7, 2026

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.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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

@OSBotify

OSBotify commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/MonilBhavsar in version: 9.4.74-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Yes — help site changes are required. Draft PR: #100634

One article covers report previews, and it never documented what pressing an expense card does — so this PR changed behavior the help site was silent about.

Article updated: docs/articles/new-expensify/reports-and-expenses/Getting-Started-with-the-Spend-Page.md

Its Report Previews section listed what a preview shows ("Up to 10 expense previews", "Carousel navigation", "+X more link if there are over 10 expenses") but nothing about pressing a card. Added:

New content Covers
## How to open a single expense from a report preview Pressing a card opens that expense in the right-hand panel with its report behind it; back returns to the report, then the workspace chat
Same section, closing paragraph The two cases where the report opens instead: a preview with only one expense, and the View button
## How to move between expenses on a report Previous/next arrows step through every expense in the order the report shows, including expenses past the rendered cards
Bullet under Preview Behavior and Updates An expense deleted while offline keeps its card until you reconnect; pressing it opens the report and the arrows skip it
FAQ entry "Why did clicking an expense card open the whole report?"

Nothing needed correcting. The existing "Up to 10 expense previews" and "+X more" lines still match the code — MAX_PREVIEWS_NUMBER = 10 — so those were left alone.

UI verification and authoring notes

I drove the web build to check every label before writing, rather than take them from the PR description:

  • The report preview action button that opens the report is View — capital V, rest lowercase — next to the green Approve button. Clicking it opened the full report.
  • Clicking an expense card opened the individual expense ($25.00 for Coffee Shop), not the parent report. Confirms the behavior this PR ships.
  • The report preview card itself shows the report name, an Outstanding status pill, 2 expenses, the cards, and a Total row.
  • The previous/next controls are unlabeled chevron icons in the expense header — no aria-label, no hover tooltip. So the article calls them "the previous and next arrows in the expense header" instead of inventing a label. The previous arrow is disabled on the first expense and the next on the last, which the article states.
  • One thing I could not verify: pressing the browser back button. The web driver has no history command, so the article describes the in-app back arrow, which I did exercise (expense → report → chat). If the browser back order differs from the in-app back order on any platform, flag it and I'll adjust the wording.

Against the three authoring docs: both new headings are task-based ## headings starting with an action verb, no ### or deeper, button names bolded in the UI's exact capitalization, and the FAQ entry is phrased as a user question. The article's pre-existing structure — its multiple # headings and noun-only section titles, which predate this PR and don't comply with the current guidelines — was left untouched to keep the diff scoped to your change. Worth a separate cleanup pass.

One thing I couldn't do: wildan-m could not be set as assignee. GitHub rejects both assignment and review requests for the account on Expensify/App ("Reviews may only be requested from collaborators"), so the draft PR has no assignee. You're @-mentioned in its body instead — please self-assign if you can, or leave it as is. The HelpDot label applied fine.

@wildan-m, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

@vincdargento

Copy link
Copy Markdown

Deploy Blocker #100669 was identified to be related to this PR

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #100673 was identified to be related to this PR.

@OSBotify

OSBotify commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.4.74-2 🚀

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

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.