Skip to content

[Payment due @ahmedGaber93] Convert Search footer totals via GetTransactionsConvertedAmount command - #94669

Merged
mountiny merged 47 commits into
Expensify:mainfrom
c3024:chaitanya/651977-get-transactions-converted-amount
Jul 30, 2026
Merged

mountiny merged 47 commits into
Expensify:mainfrom
c3024:chaitanya/651977-get-transactions-converted-amount

Conversation

@c3024

@c3024 c3024 commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

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 reuses Search'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 new searchFooterConversion Onyx 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

  1. Open Search and switch to the Expenses view, with expenses spanning more than one currency.
  2. Verify the footer shows the total spend in the default currency.
  3. Open the footer total currency picker and choose a different currency.
  4. Verify the total briefly shows a loading skeleton and then displays the converted total in the selected currency.
  5. Select a subset of expenses and verify the footer shows the selected subset total in the selected currency.
  6. Select expenses that fall beyond the first page of results and verify the total still converts correctly (the picker stays enabled).
  7. Switch to the Reports view, select one or more reports, choose a different currency, and verify the footer total converts (showing the default-currency total until the conversion arrives).
  8. Open a grouped search (e.g. group by category), choose a different currency with nothing selected and verify the grand total converts, then select one or more groups and verify the selected total converts.
  9. In that grouped search, select a whole group and also expand a different group and tick an individual transaction; verify the footer total converts and includes both the group and the individual transaction.
  10. Reset the picker to the default currency and verify the original total returns.
  11. Verify that no errors appear in the JS console.
  • Verify that no errors appear in the JS console

Offline tests

  1. Choose a custom footer currency, then go offline.
  2. Verify the currency picker does not trigger a conversion while offline and the footer keeps showing the last known total.
  3. Go back online and verify converting the footer currency works again.

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 any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • 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 any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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 Screenshot 2026-07-22 at 2 35 06 PM
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

c3024 added 3 commits June 26, 2026 12:42
…7-chaitanya/634354-search-footer-target-currency-app"

This reverts commit b3cc25d, reversing
changes made to 38dd266.
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.
@codecov

codecov Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/ONYXKEYS.ts 100.00% <ø> (ø)
src/components/ButtonComposed/Button.tsx 98.85% <100.00%> (+0.02%) ⬆️
.../Search/FilterComponents/ListFilterViewWrapper.tsx 100.00% <100.00%> (ø)
...omponents/Search/FilterComponents/SingleSelect.tsx 79.48% <ø> (ø)
...nents/Search/FilterDropdowns/SingleSelectPopup.tsx 100.00% <ø> (ø)
src/libs/API/types.ts 100.00% <ø> (ø)
src/libs/ExportOnyxState/common.ts 80.35% <ø> (ø)
src/pages/Search/SearchPage.tsx 86.79% <100.00%> (+0.25%) ⬆️
src/libs/actions/Search.ts 42.73% <90.47%> (+2.03%) ⬆️
...components/Search/FilterDropdowns/DisplayPopup.tsx 0.00% <0.00%> (ø)
... and 4 more
... and 12 files with indirect coverage changes

c3024 added 9 commits June 26, 2026 14:18
- 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).
c3024 added 3 commits July 6, 2026 12:55
…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
@c3024 c3024 changed the title [HOLD for Auth/Web] Convert Search footer totals via GetTransactionsConvertedAmount command Convert Search footer totals via GetTransactionsConvertedAmount command Jul 16, 2026
@c3024

c3024 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 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".

Comment thread src/libs/actions/Search.ts Outdated
Comment thread src/libs/actions/Search.ts Outdated
…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.
@c3024

c3024 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@c3024 c3024 changed the title Convert Search footer totals via GetTransactionsConvertedAmount command [HOLD] Convert Search footer totals via GetTransactionsConvertedAmount command Jul 16, 2026

@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: 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".

Comment thread src/components/Search/SearchSelectionFooter.tsx
Comment thread src/components/Search/SearchSelectionFooter.tsx Outdated
c3024 added 3 commits July 16, 2026 14:11
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).
@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

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 GetTransactionsConvertedAmount read command plumbing + a new searchFooterConversion Onyx 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.

Comment thread src/components/Search/SearchPageFooterSkeleton.tsx Outdated
Comment thread src/components/Search/SearchPageFooter.tsx Outdated
Comment thread src/components/Search/FilterDropdowns/CurrencyPopup.tsx

@mountiny mountiny left a comment

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.

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

Comment thread src/libs/actions/Search.ts
Comment thread src/components/Search/SearchPageFooter.tsx Outdated
c3024 added 3 commits July 30, 2026 13:50
- 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.
@c3024

c3024 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Addressed everything!

@c3024
c3024 requested a review from mountiny July 30, 2026 12:28
# Conflicts:
#	src/components/Search/FilterComponents/ListFilterViewWrapper.tsx

@mountiny mountiny left a comment

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.

Thank you

@mountiny
mountiny merged commit 7b97020 into Expensify:main Jul 30, 2026
33 checks passed
@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.

@github-actions

Copy link
Copy Markdown
Contributor

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

@OSBotify OSBotify mentioned this pull request Jul 31, 2026
67 tasks done
@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.47-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor

Help site review — no changes required

I reviewed this PR against App/docs/articles and no help site updates are needed. No draft PR was created.

Why: This is an internal re-implementation, not a user-facing change. It moves the Search footer total currency conversion onto a dedicated GetTransactionsConvertedAmount read command (replacing the reverted first-page-only snapshot approach), so the picker converts reliably for out-of-page selections, grouped views, and selected reports/transactions. The capability — picking a currency in the Search footer to convert totals — already existed before this PR; this fixes how it's computed on the backend without adding or removing anything a member sees documented.

What I checked
  • The only help-site coverage of Search total currency conversion is the group-currency: operator in Use-Search-Operators-to-Filter-and-Analyze.md. That operator's query syntax and behavior are unchanged by this PR, so the article remains accurate.
  • Search-and-Download-Expenses.md (the main Spend/Search article) does not document the footer total or its currency picker, and this PR introduces no new labels, tabs, or flows that would warrant adding it.
  • No other article references the Search footer total currency picker.

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 @MelvinBot and I'll create a draft PR.

@jponikarchuk

Copy link
Copy Markdown

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

@lanitochka17

Copy link
Copy Markdown

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

@OSBotify

OSBotify commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/marcaaron in version: 9.4.47-7 🚀

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

Bundle Size Analysis (Sentry):

@melvin-bot

melvin-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

🤖 Payment issue created: #97616

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.

9 participants