Skip to content

feat(money): add Money Account transaction toasts (MUSD-810)#30420

Merged
Kureev merged 10 commits into
mainfrom
kureev/MUSD-810
May 25, 2026
Merged

feat(money): add Money Account transaction toasts (MUSD-810)#30420
Kureev merged 10 commits into
mainfrom
kureev/MUSD-810

Conversation

@Kureev

@Kureev Kureev commented May 20, 2026

Copy link
Copy Markdown
Contributor

Description

Adds toast notifications for Money Account deposit and withdrawal transactions, mirroring the existing Earn (useEarnToasts / useMusdConversionStatus) pattern.

A new useMoneyTransactionStatus hook subscribes to TransactionController:transactionStatusUpdated and transactionConfirmed, filters for TransactionType.moneyAccountDeposit and TransactionType.moneyAccountWithdraw, deduplicates by transaction id + status, and surfaces:

  • approved → "Transaction in progress" (spinner, persistent)
  • confirmed → "Transaction complete" with the decoded mUSD amount formatted as fiat. Falls back to a X.XX mUSD label when no fiat rate is available so the toast still surfaces a real value.
  • failed → "Transaction failed" with a DSRN Primary "Try again" button. The button navigates the user to the relevant Money picker sheet (Add money / Transfer) so they can re-initiate; a true retry that preserves the prior amount/token is tracked as a follow-up.

The hook is mounted globally via <MoneyTransactionMonitor /> placed alongside <EarnTransactionMonitor /> in Nav/Main/index.js so toasts surface even after the user has navigated away from Money screens. Retry navigation goes through NavigationService (not useNavigation) because the hook runs outside the MainNavigator's screen scope.

Active transaction types covered today: moneyAccountDeposit (Convert crypto + Move mUSD) and moneyAccountWithdraw (Between accounts). Out of scope: Ramp "Deposit funds" purchases (no moneyAccountDeposit transaction is dispatched) and Perps / Predict transfers (currently "Under construction" stubs; a TODO in useMoneyTransactionStatus.ts marks where to derive the withdraw success destination once they ship).

Changelog

CHANGELOG entry: Added in-app toasts for Money Account deposit and withdrawal transactions, including a "Try again" action when a transaction fails.

Related issues

Fixes: MUSD-810

Manual testing steps

Feature: Money Account transaction toasts

  Scenario: User completes a Money Account deposit
    Given the user is on the Money home screen with a non-zero source balance
    When the user taps "Add money""Convert crypto" and confirms the conversion
    Then a "Transaction in progress" toast appears with a spinner
    And when the transaction confirms, a "Transaction complete" toast appears
    And the body reads "{amount} added to Money account."

  Scenario: User completes a Money Account withdrawal
    Given the user has a non-zero Money Account balance
    When the user taps "Transfer""Between accounts" and confirms
    Then a "Transaction in progress" toast appears with a spinner
    And when the transaction confirms, a "Transaction complete" toast appears
    And the body reads "{amount} moved to Between accounts."

  Scenario: A deposit transaction fails
    Given the user has confirmed a deposit
    When the transaction fails on-chain
    Then a "Transaction failed" toast appears with a "Try again" button
    And tapping "Try again" navigates to the Add money picker sheet

  Scenario: A withdrawal transaction fails
    Given the user has confirmed a withdrawal
    When the transaction fails on-chain
    Then a "Transaction failed" toast appears with a "Try again" button
    And tapping "Try again" navigates to the Transfer picker sheet

Screenshots/Recordings

Before

After

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
  • I've tested with a power user scenario
  • I've instrumented key operations with Sentry traces for production performance metrics

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.

Note

Medium Risk
Adds global transaction event listeners and suppresses existing in-app notifications for moneyAccountDeposit/moneyAccountWithdraw, which could impact user-facing transaction feedback and requires careful validation across transaction lifecycles (including batched transactions).

Overview
Money Account deposits/withdrawals now surface in-app toasts for approved (deferred in-progress), confirmed (success with decoded/fiat-formatted mUSD amount), and failed (error) transaction states via a new useMoneyTransactionStatus hook and MoneyTransactionMonitor mounted in Nav/Main.

This introduces a new useMoneyToasts builder for consistent toast UI + haptics and adds corresponding i18n strings, while also updating NotificationManager to skip legacy transaction notifications for moneyAccountDeposit and moneyAccountWithdraw and exporting TELLER_ABI for calldata decoding. Tests were added to cover toast option building, event subscription/dedup/timer cleanup, and batch/nested transaction handling.

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

Adds toast notifications for Money Account deposit and withdrawal
transactions, mirroring the Earn toast pattern.

- useMoneyToasts: 6 toast builders (3 deposit states + 3 withdraw states)
  with spinner / confirmation / error icons. Failed toasts include a
  'Try again' DSRN primary button wired to a retry callback.
- useMoneyTransactionStatus: subscribes to TransactionController status
  updates and confirmed events. Filters for moneyAccountDeposit /
  moneyAccountWithdraw, dedupes via shownToastsRef, decodes the requested
  mUSD amount from the teller calldata, formats it as fiat (with a mUSD
  fallback when no rate is available), and surfaces the matching toast.
  The failed-toast onRetry re-invokes initiateDeposit / initiateWithdrawal.
- MoneyTransactionMonitor: global mount point next to EarnTransactionMonitor
  in Nav/Main so toasts surface even when the user navigates away.
- en.json: new money.toasts.* keys (English only; other locales come from
  the localization pipeline).

Coverage on new files: 96.15% stmts / 85.41% branch / 100% funcs / 100% lines.
@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.

@codecov-commenter

codecov-commenter commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.76316% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.28%. Comparing base (3ee5c69) to head (197e27e).
⚠️ Report is 115 commits behind head on main.

Files with missing lines Patch % Lines
...onents/UI/Money/hooks/useMoneyTransactionStatus.ts 93.27% 0 Missing and 8 partials ⚠️
app/components/UI/Money/hooks/useMoneyToasts.tsx 89.65% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #30420      +/-   ##
==========================================
+ Coverage   82.03%   82.28%   +0.25%     
==========================================
  Files        5454     5525      +71     
  Lines      145830   148919    +3089     
  Branches    33411    34306     +895     
==========================================
+ Hits       119629   122544    +2915     
- Misses      18016    18034      +18     
- Partials     8185     8341     +156     

☔ 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.

… hook crash

`<MoneyTransactionMonitor />` is mounted as a sibling of `<MainNavigator />`
inside `app/components/Nav/Main/index.js`, so its hooks run outside the
MainNavigator's screen stack. Calling `useMoneyAccountDeposit()` /
`useMoneyAccountWithdrawal()` at the top of `useMoneyTransactionStatus`
transitively invoked `useNavigation()` (via `useConfirmNavigation`) from
that scope, which crashed the app at startup — every Android + iOS E2E
test failed to reach the wallet screen even though unit tests, build,
and lint all passed.

The retry CTA now navigates via `NavigationService.navigation.navigate`
to the Add Money / Transfer sheet, wrapped in a try/catch so a missing
nav ref logs through Logger.error instead of crashing. Tests updated to
assert `NavigationService.navigation.navigate` was called with the
appropriate route.
@Kureev Kureev self-assigned this May 20, 2026
@metamaskbotv2 metamaskbotv2 Bot added INVALID-PR-TEMPLATE PR's body doesn't match template and removed INVALID-PR-TEMPLATE PR's body doesn't match template labels May 20, 2026
@Kureev Kureev marked this pull request as ready for review May 20, 2026 07:58
@Kureev Kureev requested a review from a team as a code owner May 20, 2026 07:58
Comment thread app/components/UI/Money/hooks/useMoneyTransactionStatus.ts Outdated
Kureev added 2 commits May 20, 2026 16:09
…ure toast with design

- NotificationManager: add TransactionType.moneyAccountDeposit and
  TransactionType.moneyAccountWithdraw to SKIP_NOTIFICATION_TRANSACTION_TYPES
  so the generic pending/success/failure notifications don't fire alongside
  the Money-specific toasts. This also restores the in-progress toast's
  visibility — previously the generic pending notification was clobbering it
  immediately after it appeared.
- useMoneyToasts: replace the in-toast "Try again" button with a descriptive
  body string per the Figma failure toast (e.g. "Unable to deposit funds.
  Try again."). Titles become type-specific ("Deposit failed" / "Transfer
  failed").
- useMoneyTransactionStatus: drop the retry navigation plumbing now that
  the failure toast has no button.
- en.json: replace generic money.toasts.failed_title + try_again with
  deposit_failed_title / deposit_failed_body and withdraw_failed_title /
  withdraw_failed_body.
Comment thread app/components/UI/Money/hooks/useMoneyTransactionStatus.ts Outdated
Convert Crypto deposit failure design (Figma 7920:14709) uses
"Transaction failed" + "Unable to add funds. Try again." instead of
the more specific "Deposit failed" / "Unable to deposit funds."
Comment thread app/components/UI/Money/hooks/useMoneyTransactionStatus.ts Outdated
… tx and defer in-progress

- useMoneyTransactionStatus now subscribes directly to TransactionController
  transactionApproved / transactionFailed / transactionDropped events in
  addition to transactionStatusUpdated. The relay-strategy publish path
  (TransactionPay) doesn't always emit transactionStatusUpdated for terminal
  statuses, so the prior status-only subscription was silently missing
  failures. Earn's EarnLendingDepositConfirmationView uses the same direct
  subscription pattern for the same reason.
- Detect Money Account operations even when the parent tx type is "batch"
  (EIP-7702). isMoneyDepositTx / isMoneyWithdrawTx now inspect
  nestedTransactions for moneyAccountDeposit / moneyAccountWithdraw, mirroring
  the isMoneyActivityDeposit pattern in moneyActivityFilters.ts. Success
  amount decoding falls back to the nested entry's data when present.
- Defer the in-progress toast by 1500ms (IN_PROGRESS_DELAY_MS). Fast-confirming
  transactions (typical on Monad) no longer flash an in-progress that's
  immediately replaced by success — only the terminal toast appears. Slower
  transactions still show in-progress as before.
- Coverage: 30 unit tests including direct event paths, nested-tx detection,
  deferred-in-progress timer cancellation, and pending-timer cleanup on
  unmount.
Comment thread app/components/UI/Money/hooks/useMoneyTransactionStatus.ts
…ancelled

The transactionStatusUpdated switch only mirrored the dedicated
approved/failed handlers, so a dropped/rejected/cancelled status arriving
via statusUpdated never cancelled the deferred in-progress timer and
left a persistent "in progress" toast on a dead transaction. Mirror the
Earn useMusdConversionStatus coverage by routing all three terminal
failure statuses to showFailedFor.

@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 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

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 31088e8. Configure here.

Comment thread app/components/UI/Money/hooks/useMoneyTransactionStatus.ts
Comment thread app/components/UI/Money/hooks/useMoneyToasts.test.tsx Outdated
- Decode withdraw amount from shareAmount (decoded[1]) instead of the
  slippage floor (decoded[2]); align test encoder accordingly.
- Drop the amount phrase when calldata decoding fails so we render
  "Added to Money account." instead of " added to Money account.".
- Reuse TELLER_ABI from moneyAccountTransactions to avoid drift.
- Track cleanup timeouts and clear them on unmount.
- Use the shared mockTheme in useMoneyToasts test.
@Kureev Kureev enabled auto-merge May 21, 2026 14:49
Comment on lines +244 to +310
const handleTransactionStatusUpdated = ({
transactionMeta,
}: {
transactionMeta: TransactionMeta;
}) => {
switch (transactionMeta.status) {
case TransactionStatus.approved:
showInProgressFor(transactionMeta);
break;
case TransactionStatus.failed:
case TransactionStatus.dropped:
case TransactionStatus.rejected:
case TransactionStatus.cancelled:
showFailedFor(transactionMeta);
break;
default:
break;
}
};

const handleTransactionConfirmed = (transactionMeta: TransactionMeta) => {
if (transactionMeta.status !== TransactionStatus.confirmed) return;
showConfirmedFor(transactionMeta);
};

Engine.controllerMessenger.subscribe(
'TransactionController:transactionApproved',
handleTransactionApproved,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionFailed',
handleTransactionFailed,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionDropped',
handleTransactionDropped,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionStatusUpdated',
handleTransactionStatusUpdated,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionConfirmed',
handleTransactionConfirmed,
);

return () => {
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionApproved',
handleTransactionApproved,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionFailed',
handleTransactionFailed,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionDropped',
handleTransactionDropped,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionStatusUpdated',
handleTransactionStatusUpdated,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionConfirmed',
handleTransactionConfirmed,
);

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.

While testing I'm noticing that the generic TransactionController:transactionStatusUpdated consistently fires way before the specific events (e.g. transactionApproved).

I've tried 4 deposits and the transactionApproved subscription handler is consistently deduped.

I'm wondering if these are necessary and if we can rely purely on transactionStatusUpdated?

Let me know if you're encountering the same behaviour. Curious to hear your thoughts.

If we're able to rely solely on TransactionController:transactionStatusUpdated we may be able to remove reserveToastKey and any dedupe code.

Suggested change
const handleTransactionStatusUpdated = ({
transactionMeta,
}: {
transactionMeta: TransactionMeta;
}) => {
switch (transactionMeta.status) {
case TransactionStatus.approved:
showInProgressFor(transactionMeta);
break;
case TransactionStatus.failed:
case TransactionStatus.dropped:
case TransactionStatus.rejected:
case TransactionStatus.cancelled:
showFailedFor(transactionMeta);
break;
default:
break;
}
};
const handleTransactionConfirmed = (transactionMeta: TransactionMeta) => {
if (transactionMeta.status !== TransactionStatus.confirmed) return;
showConfirmedFor(transactionMeta);
};
Engine.controllerMessenger.subscribe(
'TransactionController:transactionApproved',
handleTransactionApproved,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionFailed',
handleTransactionFailed,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionDropped',
handleTransactionDropped,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionStatusUpdated',
handleTransactionStatusUpdated,
);
Engine.controllerMessenger.subscribe(
'TransactionController:transactionConfirmed',
handleTransactionConfirmed,
);
return () => {
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionApproved',
handleTransactionApproved,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionFailed',
handleTransactionFailed,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionDropped',
handleTransactionDropped,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionStatusUpdated',
handleTransactionStatusUpdated,
);
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionConfirmed',
handleTransactionConfirmed,
);
const handleTransactionStatusUpdated = ({
transactionMeta,
}: {
transactionMeta: TransactionMeta;
}) => {
switch (transactionMeta.status) {
case TransactionStatus.approved:
showInProgressFor(transactionMeta);
break;
case TransactionStatus.confirmed:
showConfirmedFor(transactionMeta);
break;
case TransactionStatus.failed:
case TransactionStatus.dropped:
case TransactionStatus.rejected:
case TransactionStatus.cancelled:
showFailedFor(transactionMeta);
break;
default:
break;
}
};
Engine.controllerMessenger.subscribe(
'TransactionController:transactionStatusUpdated',
handleTransactionStatusUpdated,
);
return () => {
Engine.controllerMessenger.unsubscribe(
'TransactionController:transactionStatusUpdated',
handleTransactionStatusUpdated,
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call on the dedicated transactionApproved/transactionFailed/transactionDropped subscriptions — you're right that transactionStatusUpdated fires for all of those and the dedicated ones were getting deduped every time. I've dropped all three, so we're down to just transactionStatusUpdated + transactionConfirmed now.

I kept two things from the original though, both to stay aligned with useMusdConversionStatus (the Earn equivalent):
confirmed stays on transactionConfirmed rather than moving into the switch. That event fires in sync with the TokenBalancesController balance update, so the success toast lands at the same moment the balance actually changes in the UI. If we read confirmed off transactionStatusUpdated instead, the toast can show before the balance updates. There's a comment documenting this in useMusdConversionStatus if you want the reference.

I left the dedupe (reserveToastKey) in. Even with a single source, transactionStatusUpdated can fire more than once for the same status, and now that confirmed lives on a separate event the dedupe also guards any overlap between the two. useMusdConversionStatus keeps an equivalent guard for the same reason — happy to revisit if you've seen it behave differently in testing.

Net effect is the simplification you were after (5 subscriptions → 2) without losing the balance-sync timing or the duplicate guard. Pushed in the latest commit.

nestedMatch?.data ??
(transactionMeta.txParams?.data as string | undefined);

const amountWei = decodeTellerAmount(decodeType, decodeData);

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.

Nit: amountWei is misleading and implies 18 decimals.

Suggested change
const amountWei = decodeTellerAmount(decodeType, decodeData);
const amountBaseUnit = decodeTellerAmount(decodeType, decodeData);

…terminal status

Drop the redundant transactionApproved/Failed/Dropped subscriptions; the
generic transactionStatusUpdated event fires for those statuses (matching
useMusdConversionStatus). Keep transactionConfirmed for the success toast so it
stays in sync with the balance update, and retain dedupe to guard against
repeated status-updated fires. Rename amountWei to amountBaseUnit.
@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:

The PR introduces Money account transaction monitoring infrastructure:

  1. NotificationManager.js (CRITICAL): Adds moneyAccountDeposit and moneyAccountWithdraw to SKIP_NOTIFICATION_TRANSACTION_TYPES. This suppresses standard push notifications for these transaction types since the new MoneyTransactionMonitor handles in-app toast notifications instead. This change is scoped to Money-specific transaction types and doesn't affect other transaction notification flows.

  2. Nav/Main/index.js: Adds MoneyTransactionMonitor to the main navigation layout. This is a null-rendering background component (returns null from render), so it won't visually affect other tests. The component subscribes to TransactionController events for Money-specific transaction types only.

  3. New files (MoneyTransactionMonitor, useMoneyTransactionStatus, useMoneyToasts): New Money account transaction status monitoring system that shows toast notifications for deposit/withdraw transactions (in-progress, success, failed states). Subscribes to TransactionController:transactionStatusUpdated and TransactionController:transactionConfirmed events.

  4. moneyAccountTransactions.ts: Minor export addition of TELLER_ABI.

  5. locales/en.json: New toast message strings for Money account transactions.

Tag Selection Rationale:

  • SmokeMoney: Primary tag — these changes directly implement Money account (Card/deposit/withdraw) transaction monitoring and toast notifications. The deposit/withdraw flows are core SmokeMoney functionality.
  • SmokeConfirmations: Required per SmokeMoney tag description — "When selecting SmokeMoney for Card Add Funds or similar flows that execute swaps, also select SmokeConfirmations." The Money account deposit/withdraw transactions go through the confirmation flow, and the NotificationManager change affects how confirmed transactions are handled.

No other tags are needed because:

  • The MoneyTransactionMonitor is null-rendering and only processes Money-specific transaction types
  • The SKIP_NOTIFICATION_TRANSACTION_TYPES change only adds Money-specific types, not affecting existing transaction notification flows
  • No changes to swap, stake, network, accounts, or other feature areas

Performance Test Selection:
The changes add a null-rendering background monitor component to the main navigation and new transaction event subscriptions. While there are new event listeners added via Engine.controllerMessenger.subscribe, these are lightweight and only triggered by Money-specific transaction types. No UI rendering changes, no list components, no data loading changes that would measurably impact performance metrics. The MoneyTransactionMonitor renders null and the hooks only activate on specific transaction events. No performance test tags are warranted.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@Kureev Kureev requested a review from Matt561 May 24, 2026 10:42

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

Toasts are testing well for Money account deposits 👍.

@Kureev Kureev added this pull request to the merge queue May 25, 2026
Merged via the queue into main with commit e2a3cf8 May 25, 2026
191 of 199 checks passed
@Kureev Kureev deleted the kureev/MUSD-810 branch May 25, 2026 15:01
@github-actions github-actions Bot locked and limited conversation to collaborators May 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants