feat(money): add Money Account transaction toasts (MUSD-810)#30420
Conversation
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.
|
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
… 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.
…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.
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."
… 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.
…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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 5 total unresolved issues (including 3 from previous reviews).
❌ 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.
- 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.
| 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, | ||
| ); |
There was a problem hiding this comment.
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.
| 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, | |
| ); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Nit: amountWei is misleading and implies 18 decimals.
| 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.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: The PR introduces Money account transaction monitoring infrastructure:
Tag Selection Rationale:
No other tags are needed because:
Performance Test Selection: |
|
Matt561
left a comment
There was a problem hiding this comment.
Toasts are testing well for Money account deposits 👍.




Description
Adds toast notifications for Money Account deposit and withdrawal transactions, mirroring the existing Earn (
useEarnToasts/useMusdConversionStatus) pattern.A new
useMoneyTransactionStatushook subscribes toTransactionController:transactionStatusUpdatedandtransactionConfirmed, filters forTransactionType.moneyAccountDepositandTransactionType.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 aX.XX mUSDlabel 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 />inNav/Main/index.jsso toasts surface even after the user has navigated away from Money screens. Retry navigation goes throughNavigationService(notuseNavigation) because the hook runs outside theMainNavigator's screen scope.Active transaction types covered today:
moneyAccountDeposit(Convert crypto + Move mUSD) andmoneyAccountWithdraw(Between accounts). Out of scope: Ramp "Deposit funds" purchases (nomoneyAccountDeposittransaction is dispatched) and Perps / Predict transfers (currently "Under construction" stubs; a TODO inuseMoneyTransactionStatus.tsmarks 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
Screenshots/Recordings
Before
After
Pre-merge author checklist
Performance checks (if applicable)
Pre-merge reviewer checklist
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), andfailed(error) transaction states via a newuseMoneyTransactionStatushook andMoneyTransactionMonitormounted inNav/Main.This introduces a new
useMoneyToastsbuilder for consistent toast UI + haptics and adds corresponding i18n strings, while also updatingNotificationManagerto skip legacy transaction notifications formoneyAccountDepositandmoneyAccountWithdrawand exportingTELLER_ABIfor 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.