Skip to content

feat(balances): align the total USD label with the extension ("$0.00" vs "--") - #978

Merged
CassioMG merged 5 commits into
mainfrom
cg-mobile-extension-balance-UI-parity
Aug 18, 2026
Merged

feat(balances): align the total USD label with the extension ("$0.00" vs "--")#978
CassioMG merged 5 commits into
mainfrom
cg-mobile-extension-balance-UI-parity

Conversation

@CassioMG

@CassioMG CassioMG commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Ports the extension's $0.00 vs -- rule to mobile, so both clients say the same thing about an account's total USD value.

Follows the behavior agreed in stellar/freighter#2960 — see this comment for the rule and the extension screenshots.

The rule

One helper decides it: getTotalUsdLabel in src/helpers/balances.ts, ported from the extension's helper of the same name (NO_FIAT_VALUE lives in helpers/formatAmount.ts, mirroring the extension's split).

Case Shows
Balances failed to load --
Funded, priced network, but nothing priced --
Network prices no tokens (testnet/futurenet) $0.00
Unfunded account $0.00
Anything else the total — a genuine $0.00 included

$0.00 asserts a known zero; -- means the total couldn't be determined. Both the Home header and every wallets-list row call the same helper, so the two can't disagree.

Tests: new getTotalUsdLabel suite mirrors the extension's case-for-case, plus updated coverage for the wallets list and Home header.

Screenshots

Mainnet happy path (all priced)

Screenshot 2026-08-18 at 08 32 02

Mainnet with prices erroring

Screenshot 2026-08-18 at 08 32 37

Mainnet for unfunded account

Screenshot 2026-08-18 at 08 33 42

Testnet in all cases

Always the same UI regardless of prices erroring, since we use no prices there.

Screenshot 2026-08-18 at 08 33 07

CassioMG and others added 2 commits August 17, 2026 09:48
The Tokens and Collectibles tab spinners used the near-white `secondary`
color while the Home fiat-total spinner uses `foreground.primary`, so the
three loading states on the same screen didn't match. Point both tab
spinners at `foreground.primary`.

The Tokens one also read from the hardcoded dark-only `THEME` constant;
it now goes through `themeColors`, so it's theme-aware like the other two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports the extension's getTotalUsdLabel rule (stellar/freighter#2960) so both
clients agree on when a total is a known zero and when it could not be read:

- "--" when the balances fetch failed, or a funded account on a priced
  network resolved no prices
- "$0.00" when the network prices no tokens (testnet/futurenet) or the
  account is unfunded — a real zero, not a stand-in
- the formatted total otherwise, a genuine zero included

The decision lives in one helper (helpers/balances.getTotalUsdLabel, with
NO_FIAT_VALUE in helpers/formatAmount, mirroring the extension's split) and is
used by both the Home header and the wallets list, so the two can never
disagree.

The wallets list needs inputs only its fetch cycle holds (funded state,
whether anything priced, whether the fetch failed), so accountsFiatTotals now
stores a resolved { label, hasError } entry per account instead of a nullable
BigNumber. hasError keeps a failed account eligible for the next retry, and
non-mainnet pre-fills every row with the known zero rather than leaving rows
to interpret a missing entry.

hasDecimals moved from helpers/balances to helpers/formatAmount (next to its
main caller) so the balances -> formatAmount dependency runs one way only,
matching the extension's module layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings August 18, 2026 15:27

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

Ports the extension’s total-USD display rules to mobile and aligns balance-related loading indicators.

Changes:

  • Centralizes $0.00 versus -- label selection.
  • Stores resolved per-account fiat labels and retry state.
  • Updates Home/wallet rows, spinner colors, and tests.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/hooks/useTotalBalance.ts Derives the Home total label.
src/hooks/useTokenFiatConverter/index.ts Updates hasDecimals import.
src/hooks/useSyncAccountsFiatTotals.ts Synchronizes funded/error state.
src/helpers/formatAmount.ts Adds placeholder and moves hasDecimals.
src/helpers/balances.ts Adds centralized label helper.
src/ducks/accountsFiatTotals.ts Stores and fetches resolved labels.
src/components/screens/HomeScreen/ManageAccountBottomSheet.tsx Updates fiat-total types.
src/components/screens/HomeScreen/HomeScreen.tsx Displays the resolved label.
src/components/screens/HomeScreen/AccountItemRow.tsx Renders labels and retry spinners.
src/components/CollectiblesGrid.tsx Aligns spinner color.
src/components/BalancesList.tsx Aligns spinner color.
__tests__/hooks/useSyncAccountsFiatTotals.test.tsx Updates synchronization coverage.
__tests__/helpers/getTotalUsdLabel.test.ts Tests label-selection rules.
__tests__/ducks/accountsFiatTotals.test.ts Updates totals-store coverage.
__tests__/components/screens/HomeScreen/ManageAccountBottomSheet.test.tsx Updates label fixtures.
__tests__/components/screens/HomeScreen/AccountItemRow.test.tsx Tests labels and loading states.
__tests__/components/screens/HomeScreen.test.tsx Tests Home label rendering.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/hooks/useSyncAccountsFiatTotals.ts Outdated
Comment thread src/hooks/useTotalBalance.ts
Comment thread src/ducks/accountsFiatTotals.ts
Comment thread src/ducks/accountsFiatTotals.ts
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-46dd7dd180d450924e1f (SDF collaborators only — install instructions in the release description)

The Home header showed "--" while the same account's row in the wallets sheet
showed "$0.00", which is exactly the disagreement this helper exists to prevent.

fetchedPublicKey/fetchedNetwork are stamped only on a successful response, and
clearAccountData nulls them, so a first-ever balances failure — or the first
after an account switch — left the sync guard with no proof the snapshot
belonged to this account. It returned early, the active row never got an entry,
and the row fell back to a confident "$0.00".

A failed fetch now syncs regardless of the stamp. That is safe because
getTotalUsdLabel checks hasError first and never reads the balances snapshot in
that branch, so the guard's real purpose — never attributing a previous
account's numbers to a new one — is untouched. An in-flight fetch is still
awaited, since an error is only worth writing once the request has settled.

Refs PR #978 review comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CassioMG

Copy link
Copy Markdown
Contributor Author

Code review

No issues found. Checked for bugs and AGENTS.md compliance.

🤖 Generated with Claude Code

@CassioMG
CassioMG requested review from a team and minkyeongshin August 18, 2026 16:51
@CassioMG CassioMG self-assigned this Aug 18, 2026
@piyalbasu

Copy link
Copy Markdown
Contributor

Nit: formattedBalance is now a dead export

Nit / cleanup — not a merge blocker.

TL;DR: After this change, the total-balance hook still returns the old raw formatted sum, but nothing in the app reads it anymore — it only survives in test mocks. Since the hook's own docs warn that this value can look like a confident "$0.00" when prices are missing, leaving it on the public return type is a small footgun for the next consumer.


Detailed explanation (for agents)

Root cause: HomeScreen.tsx now destructures only totalLabel from useTotalBalance(), so formattedBalance has zero production consumers. A git grep formattedBalance at head be894329d754a52b402b40b678e957cab5bacfeb matches only the hook itself and the HomeScreen.test.tsx mocks.

interface TotalBalance {
formattedBalance: string;
/**
* What the Home header should actually show: an amount, "$0.00" or "--".
* Unlike `formattedBalance` (a bare sum, which reads as a confident $0.00
* when prices are missing), this routes through {@link getTotalUsdLabel} —
* the same rule the wallets-list rows use, so the header and the active
* account's row always agree.
*/
totalLabel: string;

return {
formattedBalance: formatFiatAmount(rawBalance),
totalLabel: getTotalUsdLabel({
hasError: balancesError != null,
hasPriceFeed: isMainnet(network),
isFunded,
hasPrices: hasFiatTotal,
totalUsd: rawBalance,
}),
rawBalance,
hasFiatTotal,
};

The JSDoc on totalLabel (L11-L17) explicitly contrasts it with formattedBalance as "a bare sum, which reads as a confident $0.00 when prices are missing" — i.e. the field that remains exported is the one the docs steer consumers away from.

Suggested fixes (in increasing order of depth):

  1. Cheap: drop formattedBalance from the TotalBalance interface and the returned object, and update the HomeScreen.test.tsx mocks (L286, L342, L358, L374, L387, L399) to stop stubbing it.
  2. If a raw formatted sum is still wanted somewhere: keep it but rename/document it as internal, or derive it at the call site from rawBalance (already exported) via formatFiatAmount.

@piyalbasu piyalbasu 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.

Approved with just 1 NIT comment

CassioMG and others added 2 commits August 18, 2026 14:35
…alance

Home reads only totalLabel, so the bare formatted sum had no production
consumers left — it survived just in the test mocks. Leaving it exported is a
footgun given the hook's own docs steer consumers away from it: it reads as a
confident $0.00 when prices are missing, which is the whole reason totalLabel
exists.

Removing it also retires the formatFiatAmount import, and the totalLabel doc no
longer points at a field that isn't there.

Refs PR #978 review comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CassioMG

Copy link
Copy Markdown
Contributor Author

Fixed in d6d38f7 — took fix 1.

Confirmed it had zero production consumers (only the hook and six HomeScreen.test.tsx stubs), so formattedBalance is gone from the interface and the return, the six mocks are dropped, and the now-unused formatFiatAmount import went with it. The totalLabel doc no longer points at a field that does not exist.

One thing worth flagging while in here: rawBalance is also unused — nothing outside the hook reads it either, so fix 2's "derive it at the call site from rawBalance (already exported)" would be building on another dead export. I left it, since unlike formattedBalance it carries no footgun (a plain BigNumber rather than a string that looks like a confident total), but happy to drop it too if you would rather the hook return only what is used.

@CassioMG
CassioMG merged commit f8e8838 into main Aug 18, 2026
8 of 11 checks passed
@CassioMG
CassioMG deleted the cg-mobile-extension-balance-UI-parity branch August 18, 2026 21:47
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.

3 participants