Skip to content

feat(card): preselect Money Account and add Spend and Earn CTA on spending limit screen#30320

Merged
Brunonascdev merged 9 commits into
mainfrom
feat/card-spending-limit-money-account-preselect
May 20, 2026
Merged

feat(card): preselect Money Account and add Spend and Earn CTA on spending limit screen#30320
Brunonascdev merged 9 commits into
mainfrom
feat/card-spending-limit-money-account-preselect

Conversation

@Brunonascdev

@Brunonascdev Brunonascdev commented May 18, 2026

Copy link
Copy Markdown
Contributor

Description

This branch makes the Money Account the preferred funding source on the Card spending limit screen and adds a one-tap path to switch back to it after the user moves away.

Previous behaviour: The spending limit screen always defaulted to a regular wallet token. Even when the Money Account was funded and eligible to be the card's funding source, the user had to manually find and pick it.

New behaviour:

  1. Funded Money Account, manage / onboarding flows — The screen preselects the Money Account as the funding source. The account row shows the Money Account label + icon; the token row is locked to the Money Account token (mUSD on Linea) with the user's mUSD balance inline. Submitting calls linkMoneyAccountCard (via confirmLinkInBackground) instead of going through the regular wallet submitDelegation.
  2. User moves to a regular account — When the user selects a different account from the picker, the screen exits Money Account mode and re-derives the default token from priority / wallet balances. The SpendAndEarnPromoCard becomes visible so the user can opt back into the Money Account with one tap (re-enters Money Account mode without leaving the screen).
  3. User taps the row while in Money Account modehandleAccountSelect passes an onSelectAccount callback to the global AccountSelector via createAccountSelectorNavDetails. The callback fires on every tap inside the picker, including the tap-same-already-selected-account case (which otherwise produces no Redux change), and exits Money Account mode locally. This last bit only fully activates once the companion account-selector PR lands; everything else in this PR works standalone.
  4. enable flow — Unchanged. Money Account preselect is not applied when the user is enabling a specific new token from AssetSelectionBottomSheet.

Why

  • The Money Account is the recommended funding source when it has balance — making it the default reduces friction at delegation time and keeps spend-and-earn yield active by default.
  • Exiting Money Account mode by tapping the same regular account had no Redux signal (the AccountTreeController:selectedAccountGroupChange event only emits on real changes), so the screen needed a tap-level callback from the picker to detect it. Hook tests invoke that callback directly so they pass independently of the companion PR.
  • The Spend-and-Earn promo card surfaces the APY incentive at the exact moment the user is choosing between funding sources.

What changed (scoped paths)

Area Files / behaviour
Spending limit hook app/components/UI/Card/hooks/useSpendingLimit.ts, .test.tsisMoneyAccountSource state, preselect when funded + canLink, regular-token re-derive on exit, onSelectAccount callback wired to the picker, confirmLinkInBackground submit path.
Linkage hook app/components/UI/Card/hooks/useMoneyAccountCardLinkage.tsx, .test.tsx — exposes canLink, moneyAccountCardToken, confirmLinkInBackground, apyPercent via balance hook.
Spending limit screen app/components/UI/Card/Views/SpendingLimit/SpendingLimit.tsx, .test.tsx — Money Account variant of the account/token rows, locked token row when in Money Account mode, conditional rendering of SpendAndEarnPromoCard.
Promo card app/components/UI/Card/Views/SpendingLimit/components/SpendAndEarnPromoCard.tsx, .test.tsx, index.ts — new component.
Localization locales/languages/en.json — 7 new keys under card.card_spending_limit (Money Account label, token symbol, Spend-and-Earn copy with APY interpolation).

Out of scope (intentional)

  • Account-selector picker change. Lives in companion PR #30318 (different reviewer team). Without it, the tap-same-already-selected-account edge case stays unresolved; every other behavior in this PR works standalone.
  • Other locales beyond en.json.

Changelog

CHANGELOG entry: Preselected Money Account as the funding source on the Card spending limit screen when it has funds, and added a Spend-and-Earn promo card that lets the user switch back to Money Account in one tap.

Related issues

Fixes:

Manual testing steps

Feature: Money Account preselect on Card spending limit screen

  Background:
    Given Money Account is enabled and the user is a cardholder authenticated with the Card backend
    And the Money Account has a non-zero balance
    And the user is on the Card spending limit screen (manage or onboarding flow)

  Scenario: preselect when Money Account is funded
    Then the account row shows the Money Account label and icon
    And the token row is locked and displays "mUSD ($<balance>)"
    And the Spend-and-Earn promo card is NOT visible
    When the user taps Confirm
    Then linkMoneyAccountCard is called with the chosen delegation amount
    And the screen returns to where it came from (Card Home on onboarding, previous screen on manage)

  Scenario: switch to a regular account, then opt back into Money Account
    When the user taps the account row and selects a different account in the picker
    Then the account row shows the regular account label and avatar
    And the token row is editable and shows the priority / top-balance token
    And the Spend-and-Earn promo card IS visible (with APY subline if available)
    When the user taps the Spend-and-Earn promo card
    Then the screen re-enters Money Account mode (rows revert to Money Account variant)

  Scenario: exit Money Account by re-tapping the already-selected regular account
    Given the companion account-selector PR is merged
    When the user taps the account row and re-taps the currently-selected regular account
    Then the picker closes and the screen exits Money Account mode
    And the Spend-and-Earn promo card becomes visible

  Scenario: enable flow is unaffected
    Given the user opened the spending limit screen via AssetSelectionBottomSheet (enable flow)
    Then the Money Account is NOT preselected even if funded
    And the Spend-and-Earn promo card is NOT visible

  Scenario: empty Money Account
    Given the Money Account balance is zero
    When the user opens the spending limit screen (manage or onboarding)
    Then the regular-account default token is preselected
    And the Spend-and-Earn promo card is NOT visible

Screenshots/Recordings

Before

After

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Made with Cursor


Note

Medium Risk
Changes the Card spending-limit flow to optionally route submissions through Money Account linking (including delegation amount selection) and adds new UI state/branching, which could impact navigation and funding-source selection if edge cases are missed.

Overview
Spending limit now supports Money Account as a first-class funding source. When a linkable, funded Money Account is available (manage/onboarding only), useSpendingLimit can preselect it, lock token selection, and on submit call useMoneyAccountCardLinkage.confirmLinkInBackground (using either BAANX_MAX_LIMIT or a restricted custom limit) instead of the normal delegation path.

The SpendingLimit screen updates its account/token rows to show a Money Account variant (mUSD icon + optional fiat balance) and adds a new Spend-and-earn promo card (SpendAndEarnPromoCard + ShimmerOverlay) that appears when the user has exited Money Account mode and can switch back in one tap; the screen also adds a loading gate while the Money Account balance is resolving.

useMoneyAccountCardLinkage adjusts unauthenticated routing (only sets the pending resume flag for cardholders) and allows callers to pass an explicit delegationAmountHuman. Localization adds new card.card_spending_limit.* strings for Money Account labels and promo copy, and tests are expanded/added to cover the new UI and state transitions.

Reviewed by Cursor Bugbot for commit 0699bcb. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbotv2 metamaskbotv2 Bot added the team-card Card Team label May 18, 2026
@github-actions github-actions Bot added the pr-not-ready-for-e2e Skip E2E and block merging. Remove this label once the PR is ready to run the E2E tests. label May 18, 2026
@Brunonascdev Brunonascdev marked this pull request as ready for review May 18, 2026 17:11
@Brunonascdev Brunonascdev requested a review from a team as a code owner May 18, 2026 17:11
Comment thread app/components/UI/Card/hooks/useSpendingLimit.ts
Comment thread app/components/UI/Card/Views/SpendingLimit/SpendingLimit.test.tsx Outdated
Comment thread app/components/UI/Card/hooks/useSpendingLimit.ts Outdated
pull Bot pushed a commit to Dustin4444/metamask-mobile that referenced this pull request May 18, 2026
…every tap (MetaMask#30318)

<!--
Please submit this PR as a draft initially.

Do not mark it as "Ready for review" until this PR meets the canonical
Definition of Ready For Review in `docs/readme/ready-for-review.md`.

In short: the template must be materially complete (not just section
titles
present), all status checks must be currently passing, and the only
expected
follow-up commits must be reviewer-driven.
-->

## **Description**

This branch wires up the existing **`onSelectAccount`** route param on
`AccountSelectorParams` so callers opening the picker via
`createAccountSelectorNavDetails` receive a callback on **every** tap
inside the picker — including taps on the already-selected account.

**Previous behaviour:** The `onSelectAccount` route param was declared
on
[`AccountSelectorParams`](app/components/Views/AccountSelector/AccountSelector.types.ts)
but never invoked anywhere. Consumers that needed to know "the user
committed a selection" had to read Redux state diffs on
`AccountTreeController:selectedAccountGroupChange`, which does not emit
when the user re-taps the already-selected account. Combined with the
fact that both tap-commit and back-out paths end in
`navigation.goBack()`, callers had no signal to distinguish a
same-account commit from a dismiss.

**New behaviour:**

1. **Tap any account (different or same)** —
`_onSelectMultichainAccount` calls `setSelectedAccountGroup` (existing
behavior), then invokes the route param `onSelectAccount(accountGroup)`,
then closes. The callback fires regardless of whether the controller
actually emitted a state change.
2. **Back out / dismiss** — Unchanged: `navigation.goBack()` only;
callback does NOT fire.
3. **No callback provided** — Unchanged: picker behaves identically to
before.

Signature updated from `(address: string) => void` to `(accountGroup:
AccountGroupObject) => void` to match the multichain context. No
production caller passes this route param today (verified via repo-wide
search for `createAccountSelectorNavDetails`), so this is a safe,
additive surface change.

### Why

- Re-tapping the already-selected account inside a generic picker is a
real user signal in flows that have a "virtual" funding source layered
on top of the regular account (e.g. Money Account as Card funding source
on the spending limit screen). Without this callback, consumer screens
have no way to detect it.
- Wiring the existing param keeps the public surface unchanged and
avoids introducing a parallel naming convention.

### What changed (scoped paths)

| Area | Files / behaviour |
| ---- | ----------------- |
| **Picker route param type** |
[`app/components/Views/AccountSelector/AccountSelector.types.ts`](app/components/Views/AccountSelector/AccountSelector.types.ts)
— updated `onSelectAccount` signature to `(accountGroup:
AccountGroupObject) => void` and clarified JSDoc that it fires on every
tap. |
| **Picker wiring** |
[`app/components/Views/AccountSelector/AccountSelector.tsx`](app/components/Views/AccountSelector/AccountSelector.tsx)
— destructure `onSelectAccount` from route params and invoke inside
`_onSelectMultichainAccount` between `setSelectedAccountGroup` and
`handleClose`. |
| **Tests** |
[`app/components/Views/AccountSelector/AccountSelector.test.tsx`](app/components/Views/AccountSelector/AccountSelector.test.tsx)
— 3 new tests: callback fires with the tapped group; callback still
fires when re-tapping the already-selected account; picker does not
throw when the callback is absent. Updated existing mock's signature. |

### Out of scope (intentional)

- Other selector components that have their own component-level
`onSelectAccount` prop (`MultichainAccountSelectorList`,
`AccountListCell`, `AccountConnectSingleSelector`, etc.) — none touched.
- No changes to controllers, messengers, or default picker behavior for
any existing consumer.

## **Changelog**

CHANGELOG entry: Wired the existing `onSelectAccount` route param on the
global Account Selector picker so callers receive a callback on every
account tap (including re-tapping the already-selected account),
enabling consumer screens to distinguish a committed selection from a
dismiss.

## **Related issues**

Fixes:

<!-- Add ticket ID(s), e.g. Fixes: MUSD-xxx or MetaMask#12345 -->

Used by companion PR
[MetaMask#30320](MetaMask#30320) (Card
spending limit) to detect the "user is exiting Money Account mode by
re-tapping the same regular account" case.

## **Manual testing steps**

```gherkin
Feature: Optional onSelectAccount callback on the global Account Selector picker

  Background:
    Given a caller opens the Account Selector via createAccountSelectorNavDetails
    And the user has more than one account group

  Scenario: caller passes onSelectAccount and the user taps a different account
    When the user taps an account group different from the current one
    Then the picker calls setSelectedAccountGroup with the tapped group's id
    And the onSelectAccount callback is invoked with the tapped account group
    And the picker closes

  Scenario: caller passes onSelectAccount and the user re-taps the already-selected account
    When the user taps the account group that is already selected
    Then setSelectedAccountGroup is still called (no state change emitted)
    And the onSelectAccount callback is invoked with the tapped account group
    And the picker closes

  Scenario: caller passes onSelectAccount and the user dismisses without picking
    When the user taps the back arrow / swipes the picker down
    Then the onSelectAccount callback is NOT invoked
    And the picker closes

  Scenario: caller does NOT pass onSelectAccount (existing flows)
    Given the caller opens the picker without the onSelectAccount route param
    When the user taps any account
    Then the picker behaves identically to before (setSelectedAccountGroup + close)
    And no error is thrown
```

## **Screenshots/Recordings**

### **Before**

<!-- No visual change; behavioural only. Existing consumers (Wallet tab,
etc.) unaffected. -->

### **After**

<!-- Same picker UI. Consumers that opt in via the route param now
receive a tap signal. -->

## **Pre-merge author checklist**

<!--
Every checklist item must be consciously assessed before marking this PR
as
"Ready for review". A checked box means you deliberately considered that
responsibility, not that you literally performed every action listed.

Unchecked boxes are ambiguous: they are not an implicit "N/A" and they
are not
a silent "skip". See `docs/readme/ready-for-review.md` for the full
checklist
semantics.
-->

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

#### Performance checks (if applicable)

- [ ] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).

## **Pre-merge reviewer checklist**

<!--
Reviewer checklist items follow the same semantics as the author
checklist: an
unchecked box is ambiguous, a checked box means the reviewer consciously
assessed that responsibility. See `docs/readme/ready-for-review.md`.
-->

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the Account Selector navigation contract by invoking an
optional route callback on selection and altering its signature to pass
an `AccountGroupObject`, which could impact any existing callers and
selection flow ordering.
> 
> **Overview**
> **Account Selector now invokes an optional `onSelectAccount` route
param on every account tap.** `AccountSelector` wires
`route.params.onSelectAccount` into `_onSelectMultichainAccount`,
calling it *after* `AccountTreeController.setSelectedAccountGroup` and
*before* closing the sheet, so callers get a commit signal even when
re-tapping the already-selected account.
> 
> **API and tests updated.** `AccountSelectorParams.onSelectAccount` now
receives an `AccountGroupObject` (instead of an address string) with
updated JSDoc, and `AccountSelector.test.tsx` adds coverage for callback
firing (same/different account), ordering vs `goBack`, non-callback
behavior, dismiss via back arrow, reload flag reset, and sub-screen back
navigation.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ad94198. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Comment thread app/components/UI/Card/Views/SpendingLimit/SpendingLimit.tsx
Comment thread app/components/UI/Card/hooks/useSpendingLimit.ts

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c1ca54e. Configure here.

@Brunonascdev Brunonascdev self-assigned this May 20, 2026
@Brunonascdev Brunonascdev removed the pr-not-ready-for-e2e Skip E2E and block merging. Remove this label once the PR is ready to run the E2E tests. label May 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeMoney, SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
All 12 changed files are scoped to the MetaMask Card feature area (app/components/UI/Card/) plus locales. The changes introduce Money Account integration into the Card's SpendingLimit flow:

  1. SpendingLimit.tsx - Updated to show Money Account as a funding source option with new UI elements (money account row, locked token row, SpendAndEarnPromoCard)
  2. useSpendingLimit.ts - Extended with Money Account state (isMoneyAccountSource, canShowMoneyAccountCta, moneyAccountApyPercent, hasMetalCard, etc.)
  3. useMoneyAccountCardLinkage.tsx - New hook orchestrating Money Account → Card linkage with on-chain delegation transactions
  4. ShimmerOverlay.tsx - New animated shimmer UI component
  5. SpendAndEarnPromoCard.tsx - New promo card component for spend-and-earn feature
  6. locales/en.json - New string keys for Money Account card linkage UI

SmokeMoney is the primary tag as all changes are in the Card feature area. SmokeConfirmations is required per SmokeMoney tag description ('When selecting SmokeMoney for Card Add Funds or similar flows that execute swaps, also select SmokeConfirmations') because the Money Account linkage involves on-chain delegation transactions that go through the confirmation flow.

No changes to shared navigation, Engine, controllers, or other cross-cutting components that would warrant broader test coverage.

Performance Test Selection:
The changes are UI additions to the Card SpendingLimit screen (new components, hooks, and localization strings). While there is a new ShimmerOverlay animation component, it is a small, isolated UI element within the Card feature. The changes do not affect app startup, account/network list rendering, login flows, or other performance-critical paths. No performance tests are warranted.

View GitHub Actions results

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.68293% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.05%. Comparing base (3ee5c69) to head (0699bcb).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
...ents/UI/Card/Views/SpendingLimit/SpendingLimit.tsx 76.47% 1 Missing and 3 partials ⚠️
app/components/UI/Card/hooks/useSpendingLimit.ts 93.47% 1 Missing and 2 partials ⚠️
.../Views/SpendingLimit/components/ShimmerOverlay.tsx 95.83% 0 Missing and 1 partial ⚠️
...nents/UI/Card/hooks/useMoneyAccountCardLinkage.tsx 96.29% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #30320      +/-   ##
==========================================
+ Coverage   82.03%   82.05%   +0.02%     
==========================================
  Files        5454     5465      +11     
  Lines      145830   146314     +484     
  Branches    33411    33558     +147     
==========================================
+ Hits       119629   120063     +434     
- Misses      18016    18030      +14     
- Partials     8185     8221      +36     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sonarqubecloud

Copy link
Copy Markdown

@Brunonascdev Brunonascdev added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 9b8977f May 20, 2026
180 of 194 checks passed
@Brunonascdev Brunonascdev deleted the feat/card-spending-limit-money-account-preselect branch May 20, 2026 13:55
@github-actions github-actions Bot locked and limited conversation to collaborators May 20, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.79.0 Issue or pull request that will be included in release 7.79.0 label May 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.79.0 Issue or pull request that will be included in release 7.79.0 size-XL team-card Card Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants