fix: Add swipe to dismiss for DS Legacy toast - #33598
Conversation
|
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. |
Ignore pans while a dismiss animation is in progress, and restart auto-dismiss when entrance is interrupted by an incomplete swipe. Co-authored-by: Cursor <cursoragent@cursor.com>
Prevent a timed toast's auto-dismiss from dismissing a persistent replacement, and only resume auto-dismiss after an incomplete swipe when the spring-back finishes. Co-authored-by: Cursor <cursoragent@cursor.com>
🧪 Flaky unit test detection✅ All previously detected unit test flakiness issues in this PR have been fixed. This check is informational only and does not block merging. |
Co-authored-by: Cursor <cursoragent@cursor.com>
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 b5e58cc. Configure here.
Start auto-dismiss from incomplete-swipe onEnd instead of spring-back completion so a queued resume cannot schedule dismiss after showToast clears the previous toast. Co-authored-by: Cursor <cursoragent@cursor.com>
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection:
Toast usage across features:
SmokeConfirmations is included because SmokeSwap and SmokeMoney flows involve confirmations, and the Toast component is used during transaction flows. Performance tests: Not selected — the Toast changes are UI/gesture behavioral changes that don't affect the performance-measured scenarios (app launch, login, onboarding, asset loading, swaps execution timing). The gesture handler addition is lightweight and not in a performance-critical path. Performance Test Selection: |
|
| const closeToast = () => { | ||
| visibleAtRef.current = null; | ||
| startDismissAnimation(); | ||
| }; |
There was a problem hiding this comment.
non-blocking: The four separate useRef objects + four one-liner wrapper functions is quite verbose for what it's doing. A single stable-ref object would be easier to follow:
const swipeCallbacks = useRef({ closeToast, ensureAutoDismissAfterIncompleteSwipe, resumeAutoDismissAfterSwipe, clearScheduledAutoDismiss });
swipeCallbacks.current = { closeToast, ensureAutoDismissAfterIncompleteSwipe, resumeAutoDismissAfterSwipe, clearScheduledAutoDismiss };Then in the memoized gesture: runOnJS(() => swipeCallbacks.current.closeToast())(). Reduces 24 lines to ~2 while keeping the same semantics.
| return; | ||
| } | ||
| const { translationY, velocityY } = event; | ||
| const dismissDistance = Math.max( |
There was a problem hiding this comment.
non-blocking: The 24 minimum dismiss distance is a magic number. Worth extracting to a named constant in Toast.constants.ts alongside the other gesture thresholds, e.g. TOAST_DISMISS_MINIMUM_PX = 24.
## **Description** Adds swipe-to-dismiss to the React Native `Toaster` (top-anchored Toast), porting the same behavior from metamask-mobile. 1. **What is the reason for the change?** - Toast covers navigation elements when it appears from the top. This prevents users from accessing back buttons. - Mobile already supports swipe-to-dismiss on legacy Toast and BaseNotification. The design-system `Toaster` should match so consumers get the same interaction without reimplementing it. 2. **What is the improvement/solution?** Users can swipe a toast upward to dismiss it (by distance or quick flick). Incomplete swipes spring back and resume the auto-dismiss timer. Auto-dismiss uses an interruptible `setTimeout` instead of Reanimated `withDelay`, so pans and toast replacement can pause/clear the timer correctly. Manual close and action handlers are unchanged. Gesture thresholds match mobile (`0.35` height / `800` px/s / `-8` active offset Y / `20` fail offset X). Package adaptations: `scheduleOnRN` + explicit `'worklet'` directives for the prebuilt dist. ## **Related issues** Related: - MetaMask/metamask-mobile#33598 - MetaMask/metamask-mobile#33600 ## **Manual testing steps** 1. Run `yarn storybook:ios` (or `yarn storybook:android`) 2. Open `Components/Toast` (or Toaster) and show a timed toast 3. Swipe the toast upward past ~35% of its height — it should animate out and dismiss 4. Show another toast and flick upward quickly — it should dismiss via velocity 5. Show a toast, swipe a short distance up, and release — it should spring back and still auto-dismiss on schedule 6. Show a persistent toast (`hasNoTimeout`) and swipe to dismiss — it should dismiss; taps/buttons should still work without accidental dismiss 7. Replace a timed toast with a persistent one — the new toast should stay visible (old timer must not fire) ## **Screenshots/Recordings** ### **Before** <!-- Toast cannot be swiped away; only auto-dismiss / close --> ### **After** <!-- Toast can be swiped up to dismiss; incomplete swipe springs back --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [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 ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit bab6450. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>




Description
Adds swipe-to-dismiss to the legacy DS
Toastcomponent.Users can swipe a top toast upward to dismiss it (by distance or quick flick). Incomplete swipes spring back and resume auto-dismiss timing. Manual close and tap handlers are unchanged.
Changelog
CHANGELOG entry: Added swipe-to-dismiss for in-app toasts
Related issues
Refs: #33600
Manual testing steps
Screenshots/Recordings
Before
Toast can't be swiped
ScreenRecording_07-22-2026.00-21-44_1.MP4
After
Toast can be swiped
ScreenRecording_07-21-2026.22-51-17_1.MP4
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
Note
Medium Risk
Touches shared in-app toast animation/gesture and timer logic with several race-edge fixes; scope is UI-only but behavior is user-visible app-wide.
Overview
Adds swipe-up to dismiss on the legacy design-system
Toast, using a vertical pan gesture with tuned distance/velocity thresholds and offsets so taps and horizontal scrolls are not stolen.Auto-dismiss no longer uses Reanimated
withDelay; it uses a JSsetTimeoutthat is cleared during pans, dismiss, remeasure, and toast replacement (so an old timed toast cannot dismiss a newly shown persistent toast). Incomplete swipes spring back and resume the remaining auto-dismiss time, with guards for interrupted entrance, mid-closeToastdismiss, and stale spring-back aftershowToastreplaces content.Tests mock
react-native-gesture-handlerand cover dismiss thresholds, incomplete swipes, replacement, and dismiss-in-progress edge cases.Reviewed by Cursor Bugbot for commit 9f99e0f. Bugbot is set up for automated code reviews on this repo. Configure here.