Repository navigation
[Payment due @ahmedGaber93] Convert Search footer totals via GetTransactionsConvertedAmount command - #94669
Conversation
Replace the auxiliary-snapshot footer-currency conversion with the dedicated GetTransactionsConvertedAmount Auth command. The command merges converted figures into a new searchFooterConversion Onyx cache (by transaction and by query hash, each nested under the target currency) via onyxData, leaving the live search snapshot in its original currency. The footer reads converted totals from the cache and fetches coverage on demand only when the cache is missing what it needs, so the picker stays enabled for partial selections instead of being disabled when a selection falls outside snapshot coverage. Removes the now-obsolete buildFlatQueryWithoutGroupBy helper and the search() targetCurrency param.
…transactions-converted-amount
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
- Drop unused ConvertedAmountByCurrency/ConvertedTotalByCurrency exports - Remove the always-true currentSearchHash undefined guard - Collapse getObjectMember/getNumberMember into one getGroupCount helper - Drop the live-refresh custom-currency reset (kept the group-selection reset)
Converted footer totals are ephemeral, session-scoped display data, so drop the SEARCH_FOOTER_CONVERSION cache when the Search screen unmounts rather than persisting it across sessions. Matches the issue's reset-on-navigate-away semantics and bounds the cache to a single Search session.
On the Reports search (type:expense-report), convert a selection by report
instead of by transaction: derive the selected report IDs, read/sum their
converted totals from the new SEARCH_FOOTER_CONVERSION `reports` cache section,
and fetch uncovered report IDs via getFooterConvertedAmounts({reportIDList}).
Also stop showing the footer skeleton for a client-side subtotal (partial
selection) while the whole-search total is still loading, and only show the
conversion skeleton when there are IDs to fetch.
Selecting every loaded report made selectedExpenseCount === metadataCount, which routed the footer to the whole-search query path — invalid on the Reports search, where the backend returns report rows (no flat per-transaction rows) and errored with "No column named groupAmount". Now any explicit (loaded) report selection, including when all loaded reports are selected, converts via the per-report path. The whole-search query path is skipped on the Reports search, falling back to the default total instead of erroring or showing a stuck skeleton.
Enable the footer currency picker for grouped searches. A grouped search converts its selection by group key (new SEARCH_FOOTER_CONVERSION `groups` section, summed like reports), and the picker stays enabled on grouped views. The whole-search grand total now converts for aggregate (report/grouped) searches with nothing selected too, resolving from the cached searchTotals that every search type now returns — so the picker is no longer inert there.
A grouped search can select whole groups and individual transactions from other groups at once. Convert per row instead of treating the selection as groups only: whole-group entries from the `groups` cache, individual transactions from the `transactions` cache, summed together (reports stay aggregate-by-report). The effect fetches both the grouped totals and the loose transactions as needed. Since the whole-search grand total is now converted for every search type, all-selected falls to that grand total, so the all-selected-aggregate special case is dropped (shouldUseClientTotal = !metadataCount || hasPartialSelection).
…transactions-converted-amount
…amount # Conflicts: # src/components/Button/index.tsx # src/components/Search/FilterDropdowns/DisplayPopup.tsx # src/components/Search/FilterDropdowns/GroupCurrencyPopup.tsx # src/components/Search/SearchPageFooter.tsx # src/components/Search/SearchSelectionFooter.tsx # src/libs/actions/Search.ts
…transactions-converted-amount # Conflicts: # src/components/ButtonComposed/composed/ButtonWithIcons.tsx # src/components/Search/FilterComponents/SingleSelect.tsx # src/components/Search/SearchPageFooter.tsx # src/components/Search/SearchSelectionFooter.tsx
…transactions-converted-amount
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c852c69a6e
ℹ️ 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".
…detection Instead of writing the source stamp optimistically before the read returns (which briefly, or indefinitely on a dropped/failed read, makes a stale conversion look fresh), send the stamps to GetTransactionsConvertedAmount so it echoes them back in the same onyxData merge as the converted values. The stamp and its value are then written together and a stale entry is never shown as fresh.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3545772dc0
ℹ️ 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".
No conversion can complete offline, so an offline edit or selection change that leaves a conversion missing/stale should not flip the footer into the loading skeleton (which would stay until connectivity returns). Guard the converting state and the fetch effect with isOffline; the effect re-runs on reconnect.
A failed read leaves no converted value, so the freshness check never passes and the footer would show a loading skeleton indefinitely. Record the failed target currency (cleared when a fresh request for it is issued) and drop the converting state, falling back to the default total; the effect still retries on an edit, currency change, or reconnect.
Drops the backend source-stamp echo (Auth/Web follow-ups descoped): the App stamps sources optimistically again (in the read's optimisticData) instead of sending them to the command. Keeps the App-only offline guard and the read-failure fallback (fall back to the default total instead of a stuck skeleton or a stale converted value).
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR re-implements the Search footer’s “total spend” currency picker using a dedicated read command (GetTransactionsConvertedAmount) and a session-scoped Onyx cache, so footer totals can be converted on-demand for full searches and selections (including out-of-page and grouped selections) without relying on auxiliary snapshots.
Changes:
- Add
GetTransactionsConvertedAmountread command plumbing + a newsearchFooterConversionOnyx cache/type to store converted footer totals and source stamps for staleness detection. - Update the Search selection footer + footer UI to request missing conversions, show a skeleton while converting, and support currency selection via a shared
CurrencyPopup. - Extend UI infrastructure (fixed-height popover support, button focus/blur handling) and add a unit test for concurrent search request deduping.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/Search/searchTotalsLoadingDataTest.ts | Updates mock typing and adds coverage ensuring concurrent identical Search requests are deduped. |
| src/types/onyx/SearchFooterConversion.ts | Introduces the Onyx type for the footer conversion cache (transactions/reports/groups/searchTotals + source stamps + failure flags). |
| src/types/onyx/index.ts | Exports the new SearchFooterConversion type. |
| src/pages/Search/SearchPage.tsx | Clears the session-scoped footer conversion cache when leaving Search. |
| src/ONYXKEYS.ts | Adds SEARCH_FOOTER_CONVERSION key and Onyx values mapping. |
| src/libs/ExportOnyxState/common.ts | Masks SEARCH_FOOTER_CONVERSION during Onyx export. |
| src/libs/API/types.ts | Registers GET_TRANSACTIONS_CONVERTED_AMOUNT as a read command + parameter mapping. |
| src/libs/API/parameters/index.ts | Re-exports GetTransactionsConvertedAmount parameter type. |
| src/libs/API/parameters/GetTransactionsConvertedAmount.ts | Defines parameter typing for the new read command. |
| src/libs/actions/Search.ts | Refactors backend query serialization and adds getFooterConvertedAmounts() + clearFooterConversion(). |
| src/components/Search/SearchSelectionFooter.tsx | Implements conversion-aware footer totals logic (selection-aware fetching, caching, staleness detection, loading state). |
| src/components/Search/SearchPageFooterSkeleton.tsx | Adds a dedicated skeleton component for footer total loading/convert states. |
| src/components/Search/SearchPageFooter.tsx | Adds footer currency picker UI (button + popover), integrates skeleton overlay, and handles Enter shortcut behavior. |
| src/components/Search/FilterDropdowns/SingleSelectPopup.tsx | Adds support for a fixed popover height while filtering results. |
| src/components/Search/FilterDropdowns/GroupCurrencyPopup.tsx | Removes bespoke group-currency popup in favor of the shared currency popup. |
| src/components/Search/FilterDropdowns/DisplayPopup.tsx | Switches group-currency selection to use CurrencyPopup. |
| src/components/Search/FilterDropdowns/CurrencyPopup.tsx | Adds a shared searchable currency picker popup used across Search UIs. |
| src/components/Search/FilterComponents/SingleSelect.tsx | Threads fixed-height popover support down to the underlying list wrapper. |
| src/components/Search/FilterComponents/ListFilterViewWrapper.tsx | Implements fixed-height behavior for selection list popovers. |
| src/components/ButtonComposed/types.ts | Adds focus/blur event props to the composed button type. |
| src/components/ButtonComposed/Button.tsx | Wires focus/blur props through to the underlying pressable component. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mountiny
left a comment
There was a problem hiding this comment.
Minor thing but the first time you open the modal on a page, it kinda translates to the view
https://github.com/user-attachments/assets/5d8b451a-3c5f-43dc-a0f8-05b933b256fb
Looking good and tests well, great job @c3024 ❤️
Can you address couple of the comments left on the pr now? thanks
- Hoist SearchPageFooterSkeleton's inline container style to a module-level constant - Use a stable no-op callback for the total button's Enter shortcut so it doesn't resubscribe every render - Update CurrencyPopup's comment to note it's also used by the Display filter's group-currency picker
Addresses mountiny's PR review comment asking for a unit test on this function. Covers the empty-currency no-op, request param shaping (transaction/report ID scoping), and the optimisticData/failureData stamp on SEARCH_FOOTER_CONVERSION.
|
Addressed everything! |
# Conflicts: # src/components/Search/FilterComponents/ListFilterViewWrapper.tsx
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.47-0 🚀
|
Help site review — no changes requiredI reviewed this PR against Why: This is an internal re-implementation, not a user-facing change. It moves the Search footer total currency conversion onto a dedicated What I checked
If you feel the footer currency picker itself is worth documenting as a net-new "how to" (independent of this re-implementation), let me know and I'll draft an article — but that would be a new-content request, not something this PR changes. @c3024, please confirm you agree no help site changes are required here. If you'd instead like me to document the footer currency picker, reply with |
|
Deploy Blocker #97579 was identified to be related to this PR. |
|
Deploy Blocker #97583 was identified to be related to this PR. |
|
🚀 Deployed to production by https://github.com/marcaaron in version: 9.4.47-7 🚀
Bundle Size Analysis (Sentry): |
|
🤖 Payment issue created: #97616 |
Explanation of Change
Re-implements the Search footer currency picker on a dedicated read command,
GetTransactionsConvertedAmount(agreed in Expensify #651977), replacing the auxiliary-snapshot approach from #90777 (reverted in #94562) whose first-page-only coverage forced the picker to disable for out-of-page selections. The command reusesSearch's query/aggregation (so numbers match) and converts on demand — whole search, selected transactions (transactionIDList), selected reports (reportIDList), or a grouped view's groups + grand total — merging results into a newsearchFooterConversionOnyx cache without touching the live snapshot. The footer reads that cache (fetching only what's missing), converts per row so a grouped selection can mix groups and loose transactions, works with nothing selected, and fetches again when an inline edit moves a stamped source value.Backend (both merged): Auth #22515, Web #54039.
Fixed Issues
$ https://github.com/Expensify/Expensify/issues/634354
https://github.com/Expensify/Expensify/issues/651977
PROPOSAL:
Tests
Offline tests
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
Screen.Recording.2026-07-22.at.2.37.13.PM.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-07-22.at.12.29.12.PM.mov
Screen.Recording.2026-07-22.at.12.20.11.PM.mov
Screen.Recording.2026-07-22.at.12.13.13.PM.mov
Screen.Recording.2026-07-22.at.12.11.55.PM.mov
Screen.Recording.2026-07-22.at.12.10.36.PM.mov
Screen.Recording.2026-07-22.at.12.00.34.PM.mov
Screen.Recording.2026-07-22.at.11.57.59.AM.mov
Screen.Recording.2026-07-21.at.6.18.03.PM.mov
Screen.Recording.2026-07-21.at.6.15.52.PM.mov
Screen.Recording.2026-07-21.at.5.57.00.PM.mov
Screen.Recording.2026-07-21.at.5.51.10.PM.mov
Screen.Recording.2026-07-21.at.5.45.25.PM.mov