Skip to content

fix(balance): $0-balance fix for the mobile build (cherry-pick of #2482) - #2485

Closed
innolope-dev wants to merge 4 commits into
chore/mobile-release-syncfrom
cherry/balance-fix-mobile-release
Closed

fix(balance): $0-balance fix for the mobile build (cherry-pick of #2482)#2485
innolope-dev wants to merge 4 commits into
chore/mobile-release-syncfrom
cherry/balance-fix-mobile-release

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Cherry-picks the three commits from #2482 onto the mobile-release sync branch, so the next build carries the $0-balance fix.

Targets chore/mobile-release-sync (#2483), not mobile-release directly — the balance commits expect main's balance.utils / rain.ts context, which the sync branch has and raw mobile-release does not. Merge order: #2483 → this. Once both land on mobile-release, the build has: main sync + localization (#2483) + this fix.

What it does

Same as #2482 (targeting main) — see there for the full write-up. In short: a failed/again-missing /rain/cards read made a card user's spendable total collapse to a confident $0; this persists the last-known-good total, consumes the API's balanceUnavailable flag, stops polling /rain/cards for non-card users, and stops rendering unknown spend/limit figures as $0.

Cherry-pick conflicts (2 files)

Both are files the localization branch in #2483 also touched:

  • features/limits/utils.ts — the sync branch has a newer structure (amountDisplay, perTransactionSuffix, richer items.push with a kind discriminator for translation). Re-applied the fix's intent onto that shape: formattedLimit/amountDisplay go null when the remaining limit is unknown, and the amount item is omitted entirely rather than showing "$0".
  • utils/__tests__/balance.utils.test.ts — pure additive overlap: the sync branch added a computeExcessCollateralCents describe block, fix(balance): keep last-known spendable total instead of rendering $0 #2482 added an isRainBalanceKnown one, both at the file tail. Kept both.

One tree-specific adaptation (separate commit)

useWalletSpendable.test.tsx carried // eslint-disable-next-line import/first directives — fine on the main-based #2482 tree, but this tree's eslint config doesn't register import/first, so disabling an unknown rule is itself an error. Dropped them for the sibling tests' plain-comment convention. This keeps the branch at 0 eslint errors (#2483's headline).

Testing

  • Full suite: 165 suites, 2162 passed, 3 skipped
  • eslint . 0 errors (62 warnings, baseline); prettier clean; typecheck clean

Depends on

FE degrades gracefully without the API change, but pairs with peanutprotocol/peanut-api-ts#1227 (balanceUnavailable).

The home screen's headline number is smart-account balance + Rain
spendingPower + Rain inTransit. Both Rain terms come from a single
/rain/cards call, and rainCentsToUsdcUnits(undefined) folds them to 0n —
so a failed fetch was indistinguishable from 'this user has no
collateral'. For a card user whose funds the auto-balancer swept into
collateral, the smart account is legitimately 0, and the total cratered
to a confident, wrong $0 with no error and no retry affordance.

/rain/cards times out at 10s for 621 users (PEANUT-UI-QD5), on web and
native alike; native compounds it because the Android WebView GET path
is edge-blocked on some devices (PEANUT-UI-R5F).

React Query retains the last good data across a failed refetch, so this
only bit on a session's first load. Persist the last fully-settled total
per user and seed the display from it, so a cold start paints the
previous number immediately and corrects it in the background. The
cached value is display-only and dimmed while stale — every
affordability gate still runs on the live availableSpendableBalance, so
a stale number can never green-light a spend into an orphan charge.
…r non-card users

Follow-on to the last-known-spendable cache, pairing with the API's new
`balanceUnavailable` flag.

- treat a response that arrived but couldn't read Rain (flag set, no
  balance to fall back on) as not-ready, so the displayed total holds the
  last-known value instead of summing an absent spendingPower as 0. A
  null balance WITHOUT the flag stays a real zero — a user with no card
  must still see their plain smart-account total.
- a served-but-stale balance is used as-is: it is fresher than the FE's
  own cache.
- stop the 30s /rain/cards poll for users with no card application. It
  ran for every logged-in user and made this the most-called endpoint,
  and with it the top source of client-side 10s timeouts, for a payload
  that reads `hasApplication: false` every time. Card users keep the 30s
  cadence, and the user_rail_status_changed WebSocket invalidation
  resumes polling the moment someone applies.
- guard on `!!rainOverview` rather than `!== undefined`: a null JSON body
  would pass the stricter check and then throw on property access.
…limits as $0

Two more instances of the same unknown-coerced-to-zero shape.

- extract isRainBalanceKnown into balance.utils so the display path
  (useWallet) and the spend path (useSpendBundle) answer 'can this Rain
  figure be trusted' identically. They were never allowed to disagree.
- spendPreflight: when the overview is missing, rainSpendingPower arrives
  as 0n and a Rain outage lands in the insufficient branch. Blocking is
  still the right call — we can't size a collateral withdrawal we can't
  see, and the user already gets BALANCE_SETTLING_MESSAGE ('try again in
  a few seconds'), not a false 'not enough funds'. But reporting it as
  error_kind 'insufficient' hid the outage inside the card-withdraw
  failure rate; it now reports 'rain-balance-unavailable'.
- limits: 'You can add up to $0' when remainingLimit is merely unknown.
  Omit the sentence rather than invent a figure.
…s tree

The cherry-picked useWalletSpendable.test.tsx carried
`// eslint-disable-next-line import/first` directives (fine on the
main-based #2482 tree). The mobile-release eslint config doesn't
register import/first, so disabling an unknown rule is itself an error
here — it was the only thing pushing eslint back above 0. Match the
sibling tests' plain-comment convention.
@innolope-dev innolope-dev self-assigned this Jul 22, 2026
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 22, 2026 11:30pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59a1b06f-c5d8-44c6-aaeb-be2f098af00e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cherry/balance-fix-mobile-release

Comment @coderabbitai help to get the list of available commands.

@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Superseded by direct merges to mobile-release (no PR needed for the release branch): the original fix rode the 1.0.36 sync (91e6c73), and the follow-up hardening from #2482 (owner-scoped in-memory holds + no cache writes from balanceUnavailable data) is merged in 5ab2c2a.

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.

1 participant