feat(card): preselect Money Account and add Spend and Earn CTA on spending limit screen#30320
Conversation
…nding limit screen
|
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. |
…money account CTA
…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 -->
…card-spending-limit-money-account-preselect
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection:
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: |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
|




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:
linkMoneyAccountCard(viaconfirmLinkInBackground) instead of going through the regular walletsubmitDelegation.SpendAndEarnPromoCardbecomes visible so the user can opt back into the Money Account with one tap (re-enters Money Account mode without leaving the screen).handleAccountSelectpasses anonSelectAccountcallback to the globalAccountSelectorviacreateAccountSelectorNavDetails. 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 companionaccount-selectorPR lands; everything else in this PR works standalone.enableflow — Unchanged. Money Account preselect is not applied when the user is enabling a specific new token fromAssetSelectionBottomSheet.Why
AccountTreeController:selectedAccountGroupChangeevent 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.What changed (scoped paths)
app/components/UI/Card/hooks/useSpendingLimit.ts,.test.ts—isMoneyAccountSourcestate, preselect when funded +canLink, regular-token re-derive on exit,onSelectAccountcallback wired to the picker,confirmLinkInBackgroundsubmit path.app/components/UI/Card/hooks/useMoneyAccountCardLinkage.tsx,.test.tsx— exposescanLink,moneyAccountCardToken,confirmLinkInBackground,apyPercentvia balance hook.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 ofSpendAndEarnPromoCard.app/components/UI/Card/Views/SpendingLimit/components/SpendAndEarnPromoCard.tsx,.test.tsx,index.ts— new component.locales/languages/en.json— 7 new keys undercard.card_spending_limit(Money Account label, token symbol, Spend-and-Earn copy with APY interpolation).Out of scope (intentional)
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
Screenshots/Recordings
Before
After
Pre-merge author checklist
Performance checks (if applicable)
trace()for usage andaddTokenfor an exampleFor performance guidelines and tooling, see the Performance Guide.
Pre-merge reviewer checklist
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),
useSpendingLimitcan preselect it, lock token selection, and on submit calluseMoneyAccountCardLinkage.confirmLinkInBackground(using eitherBAANX_MAX_LIMITor a restricted custom limit) instead of the normal delegation path.The
SpendingLimitscreen 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.useMoneyAccountCardLinkageadjusts unauthenticated routing (only sets the pending resume flag for cardholders) and allows callers to pass an explicitdelegationAmountHuman. Localization adds newcard.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.