Skip to content

feat(react): Toast component with Toaster and imperative toast() API#1190

Merged
georgewrmarshall merged 26 commits into
mainfrom
toast-dsr
Jun 4, 2026
Merged

feat(react): Toast component with Toaster and imperative toast() API#1190
georgewrmarshall merged 26 commits into
mainfrom
toast-dsr

Conversation

@georgewrmarshall

@georgewrmarshall georgewrmarshall commented May 27, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the Toast component pair for React: a presentational Toast, a root-mounted Toaster, and the imperative toast() / toast.dismiss() API.

The implementation keeps toast behavior aligned with React Native while staying web-native in its rendering and placement model. Shared toast severity and timing live in @metamask/design-system-shared; platform-specific icon and layout concerns stay local.

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-514

Manual testing steps

  1. Open the React Storybook toast docs and verify the static Toast examples render correctly.
  2. Trigger toast(...) from the docs example and confirm the Toaster mounts, animates in, auto-dismisses, and can be dismissed manually.
  3. Test the extension preview package to confirm the toast renders in the extension shell and matches the expected bottom placement.

Screenshots/Recordings

After

Toast component docs and stories

toast.after720.mov

Toast component covers extension use cases. Showing preview package Toast usage working in the extension
PR: MetaMask/metamask-extension#43122

toast.extension.loading720.mov
toast.extension.success720.mov

More toasts from extension click to expand image

toast-vr-01-buy-tab-opened-default-action-1780516908376toast-vr-02-infura-switch-success-1780517017302toast-vr-03-storage-error-danger-action-1780517072275toast-vr-04-perps-deposit-pending-1780517095224toast-vr-05-perps-withdraw-success-1780517115977toast-vr-06-perps-error-1780517147106toast-vr-07-musd-conversion-success-1780517172211toast-vr-08-merkl-claim-failed-1780517192261toast-vr-09-survey-default-action-1780517253910toast-vr-probe-screenshot-endpoint-1780517034153

Toast comparison between both platforms is aligned

toast.react.reactnative.720.mov

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs
  • I've completed the PR template to the best of my ability
  • I’ve included tests if applicable
  • I’ve documented my code using JSDoc format if applicable
  • I’ve applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

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
New public imperative API with module-level registration and timer/animation lifecycle; risk is mostly integration misuse (e.g. missing Toaster), mitigated by tests and clear errors—not security-critical paths.

Overview
Adds React Toast support: a Toast surface on BannerBase, a root Toaster, and toast() / toast.dismiss() for app-wide notifications with slide-up animation, auto-dismiss (~2750 ms), and single-toast replacement (no queue).

ToastSeverity and timing constants (TOAST_ANIMATION_DURATION, TOAST_VISIBILITY_DURATION) move into @metamask/design-system-shared; React Native Toast re-exports those instead of defining them locally. Web keeps a local severity→icon map until a shared alert icon exists.

Ships Storybook docs, package exports from design-system-react, and broad unit tests for presentation, timers, replacement, unmount cleanup, and pre-mount errors.

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

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

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

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Auto-dismiss effect cleanup cancels exit animation timer
    • Introduced separate refs for auto-dismiss and exit cleanup timers to prevent the effect cleanup from clearing the exit timer.
  • ✅ Fixed: Raw animation duration instead of design token
    • Replaced the hardcoded 200ms with AnimationDuration.Regularly from @metamask/design-tokens.
  • ✅ Fixed: Missing requestAnimationFrame cleanup enables stale visibility update
    • Captured rAF IDs and canceled them in the effect cleanup (and on close) to prevent stale visibility updates.

Create PR

Or push these changes by commenting:

@cursor push 89e3e504a1
Preview (89e3e504a1)
diff --git a/packages/design-system-react/src/components/Toast/Toast.constants.ts b/packages/design-system-react/src/components/Toast/Toast.constants.ts
--- a/packages/design-system-react/src/components/Toast/Toast.constants.ts
+++ b/packages/design-system-react/src/components/Toast/Toast.constants.ts
@@ -1,10 +1,11 @@
 import { ToastSeverity } from '@metamask/design-system-shared';
+import { AnimationDuration } from '@metamask/design-tokens';
 
 import { IconColor, IconName } from '../../types';
 
 export const TOAST_VISIBILITY_DURATION = 2750;
 /** Duration of the enter/exit CSS transition in milliseconds. */
-export const TOAST_ANIMATION_DURATION = 200;
+export const TOAST_ANIMATION_DURATION = AnimationDuration.Regularly;
 
 export const TOAST_SEVERITY_ICON_MAP = {
   [ToastSeverity.Success]: {

diff --git a/packages/design-system-react/src/components/Toast/Toaster.tsx b/packages/design-system-react/src/components/Toast/Toaster.tsx
--- a/packages/design-system-react/src/components/Toast/Toaster.tsx
+++ b/packages/design-system-react/src/components/Toast/Toaster.tsx
@@ -48,21 +48,44 @@
     const replacementTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
       null,
     );
-    const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+    // Separate timers to avoid cleanup collisions.
+    const autoDismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
+      null,
+    );
+    const exitCleanupTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
+      null,
+    );
+    // Track pending rAF callbacks for enter animation.
+    const enterRafId1Ref = useRef<number | null>(null);
+    const enterRafId2Ref = useRef<number | null>(null);
     const innerRef = useRef<ToasterRef | null>(null);
 
     const closeToast = () => {
+      // Cancel any pending enter rAFs to prevent stale visibility updates.
+      if (enterRafId1Ref.current !== null) {
+        cancelAnimationFrame(enterRafId1Ref.current);
+        enterRafId1Ref.current = null;
+      }
+      if (enterRafId2Ref.current !== null) {
+        cancelAnimationFrame(enterRafId2Ref.current);
+        enterRafId2Ref.current = null;
+      }
       if (replacementTimerRef.current !== null) {
         clearTimeout(replacementTimerRef.current);
         replacementTimerRef.current = null;
       }
-      if (dismissTimerRef.current !== null) {
-        clearTimeout(dismissTimerRef.current);
-        dismissTimerRef.current = null;
+      // Clear any auto-dismiss timer and any prior exit cleanup timer.
+      if (autoDismissTimerRef.current !== null) {
+        clearTimeout(autoDismissTimerRef.current);
+        autoDismissTimerRef.current = null;
       }
+      if (exitCleanupTimerRef.current !== null) {
+        clearTimeout(exitCleanupTimerRef.current);
+        exitCleanupTimerRef.current = null;
+      }
       setIsVisible(false);
-      dismissTimerRef.current = setTimeout(() => {
-        dismissTimerRef.current = null;
+      exitCleanupTimerRef.current = setTimeout(() => {
+        exitCleanupTimerRef.current = null;
         setToastOptions(undefined);
       }, TOAST_ANIMATION_DURATION);
     };
@@ -73,10 +96,15 @@
 
       if (toastOptions) {
         setIsVisible(false);
-        if (dismissTimerRef.current !== null) {
-          clearTimeout(dismissTimerRef.current);
-          dismissTimerRef.current = null;
+        // Clear any existing timers when replacing an in-flight toast.
+        if (autoDismissTimerRef.current !== null) {
+          clearTimeout(autoDismissTimerRef.current);
+          autoDismissTimerRef.current = null;
         }
+        if (exitCleanupTimerRef.current !== null) {
+          clearTimeout(exitCleanupTimerRef.current);
+          exitCleanupTimerRef.current = null;
+        }
         timeoutDuration = TOAST_ANIMATION_DURATION;
         setToastOptions(undefined);
       }
@@ -107,24 +135,36 @@
     // Trigger enter animation after toast is mounted in the DOM.
     useEffect(() => {
       if (toastOptions && !isVisible) {
-        requestAnimationFrame(() => {
-          requestAnimationFrame(() => {
+        enterRafId1Ref.current = requestAnimationFrame(() => {
+          enterRafId2Ref.current = requestAnimationFrame(() => {
             setIsVisible(true);
           });
         });
+        // Cleanup to cancel pending rAFs if toast is dismissed quickly.
+        return () => {
+          if (enterRafId1Ref.current !== null) {
+            cancelAnimationFrame(enterRafId1Ref.current);
+            enterRafId1Ref.current = null;
+          }
+          if (enterRafId2Ref.current !== null) {
+            cancelAnimationFrame(enterRafId2Ref.current);
+            enterRafId2Ref.current = null;
+          }
+        };
       }
+      return undefined;
     }, [toastOptions]); // intentionally omit isVisible — only react to new toast options
 
     // Auto-dismiss timer.
     useEffect(() => {
       if (isVisible && toastOptions && !toastOptions.hasNoTimeout) {
-        dismissTimerRef.current = setTimeout(() => {
-          dismissTimerRef.current = null;
+        autoDismissTimerRef.current = setTimeout(() => {
+          autoDismissTimerRef.current = null;
           innerRef.current?.closeToast();
         }, TOAST_VISIBILITY_DURATION);
         return () => {
-          if (dismissTimerRef.current !== null) {
-            clearTimeout(dismissTimerRef.current);
+          if (autoDismissTimerRef.current !== null) {
+            clearTimeout(autoDismissTimerRef.current);
           }
         };
       }

You can send follow-ups to the cloud agent here.

Comment thread packages/design-system-react/src/components/Toast/Toaster.tsx
Comment thread packages/design-system-react/src/components/Toast/Toast.constants.ts Outdated
Comment thread packages/design-system-react/src/components/Toast/Toaster.tsx
@georgewrmarshall georgewrmarshall self-assigned this May 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/design-system-react": "0.24.0-preview.f5c2542",
  "@metamask-previews/design-system-react-native": "0.27.0-preview.f5c2542",
  "@metamask-previews/design-system-shared": "0.20.0-preview.f5c2542",
  "@metamask-previews/design-system-tailwind-preset": "0.8.0-preview.f5c2542",
  "@metamask-previews/design-system-twrnc-preset": "0.4.2-preview.f5c2542",
  "@metamask-previews/design-tokens": "8.4.0-preview.f5c2542"
}

export { Toast } from './Toast';
export { Toaster, toast } from './Toaster';
export { ToastSeverity } from './Toast.types';
export { ToastSeverity } from '@metamask/design-system-shared';

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.

Moved ToastSeverity to shared

Comment on lines -6 to -17
/**
* Toast severity variants.
* `Default` renders no built-in leading icon.
*/
export const ToastSeverity = {
Default: 'default',
Success: 'success',
Warning: 'warning',
Danger: 'danger',
} as const;

export type ToastSeverity = (typeof ToastSeverity)[keyof typeof ToastSeverity];

@georgewrmarshall georgewrmarshall Jun 1, 2026

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.

Moved ToastSeverity to shared as it is used by both React and React Native versions of the Toast

Comment on lines -11 to +15
export {
TOAST_VISIBILITY_DURATION,
TOAST_ANIMATION_DURATION,
TOAST_BOTTOM_PADDING,
} from './Toast.constants';
export { TOAST_BOTTOM_PADDING } from './Toast.constants';

@georgewrmarshall georgewrmarshall Jun 1, 2026

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.

Moved TOAST_VISIBILITY_DURATION and TOAST_ANIMATION_DURATION to shared as they are used by both React Native and React versions of the Toast

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

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Animation duration silently changed from 300ms to 200ms
    • Updated shared TOAST_ANIMATION_DURATION to AnimationDuration.Regularly (300ms) to match prior React Native behavior.

Create PR

Or push these changes by commenting:

@cursor push 583134f2fc
Preview (583134f2fc)
diff --git a/packages/design-system-shared/src/types/Toast/Toast.constants.ts b/packages/design-system-shared/src/types/Toast/Toast.constants.ts
--- a/packages/design-system-shared/src/types/Toast/Toast.constants.ts
+++ b/packages/design-system-shared/src/types/Toast/Toast.constants.ts
@@ -4,4 +4,4 @@
  * Shared toast timing constants.
  */
 export const TOAST_VISIBILITY_DURATION = 2750;
-export const TOAST_ANIMATION_DURATION = AnimationDuration.Promptly;
+export const TOAST_ANIMATION_DURATION = AnimationDuration.Regularly;

You can send follow-ups to the cloud agent here.

Comment thread packages/design-system-shared/src/types/Toast/Toast.constants.ts Outdated
};
```

`<Toaster />` must be rendered exactly once. On mount it registers the `toast(...)` / `toast.dismiss()` API so it can be called from anywhere in your app.

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.

This README is primarily documenting the toast() API because that is the consumer entry point. The inline <Toast /> examples stay here to make the shape easier to scan, but the code snippets are the canonical usage for the imperative flow.

}
: undefined;

// Toast reuses BannerBase so the web surface stays aligned with the shared

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.

Toast reuses BannerBase so the web surface stays aligned with the shared banner layout and the React Native Toast API. That keeps the consumer contract consistent even though the platform primitives differ.

export default meta;
type Story = StoryObj<ToastProps>;

export const Default: Story = {

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.

These stories intentionally render Toast directly where that makes the component easier to review in Storybook. The imperative toast(...) flow is still covered by the default story, so this keeps the docs focused without duplicating the click-to-show plumbing everywhere.


import { IconColor, IconName } from '../../types';

// TODO: Replace this map with a web IconAlert component once the shared

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.

TOAST_SEVERITY_ICON_MAP is a temporary web-only bridge until React has an IconAlert equivalent. Keeping it local avoids leaking a platform-specific icon abstraction into shared code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just wanted to write a comment that we could unify the codebase 😁 But yes, your comment explains why we made it like this 👍

TOAST_BOTTOM_PADDING,
TOAST_VISIBILITY_DURATION,
} from './Toast.constants';
import { TOAST_BOTTOM_PADDING } from './Toast.constants';

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.

TOAST_BOTTOM_PADDING is RN specific so leaving it in constants


let registeredRef: RefObject<ToasterRef | null> | null = null;

const assertRegisteredRef = (method: 'dismiss' | 'toast'): ToasterRef => {

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.

Calling toast() before <Toaster /> mounts throws immediately instead of silently no-oping. That gives consumers an explicit integration error instead of hiding a missing root provider. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts

}, TOAST_ANIMATION_DURATION);
};

// Replace the currently mounted toast rather than queueing multiple toasts.

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.

This intentionally replaces the current toast instead of queueing multiple items. It matches the current single-toast service model, and the same host can be extended to stack later without changing the consumer API. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx

};
}, []);

// Delay the enter transition until after mount so the DOM can paint the

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.

The double requestAnimationFrame and the separate auto-dismiss timer are doing the same job a toast viewport does in libraries like react-hot-toast: mount offscreen, animate in, keep visible for a fixed duration, then clear after exit. The outer container owns placement and lifecycle while Toast stays presentational. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx https://react-hot-toast.com/

ToasterRef,
} from './Toast.types';

let registeredRef: RefObject<ToasterRef | null> | null = null;

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.

This follows the same mounted-host pattern used by react-hot-toast: a single Toaster instance registers the imperative API once, and callers interact with that host instead of managing toast state directly. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx


const ToasterComponent = forwardRef<ToasterRef, ToasterProps>(
({ className, ...props }, ref) => {
const [toastOptions, setToastOptions] = useState<ToastOptions | undefined>(

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.

The state is split into payload, visibility, replacement, auto-dismiss, exit cleanup, and enter animation refs so each phase can be controlled independently. That separation is what keeps dismiss and replacement from racing each other. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx

TOAST_VISIBILITY_DURATION,
} from './Toast.constants';

describe('Toast.constants', () => {

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.

This test is needed because design-system-shared enforces 100% coverage on runtime exports, and a new constants module would otherwise drop the package below threshold. Keeping the assertion here preserves the gate without weakening coverage rules.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall marked this pull request as ready for review June 3, 2026 15:35
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner June 3, 2026 15:35
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/design-system-react": "0.24.0-preview.a0fe3894",
  "@metamask-previews/design-system-react-native": "0.27.0-preview.a0fe3894",
  "@metamask-previews/design-system-shared": "0.20.0-preview.a0fe3894",
  "@metamask-previews/design-system-tailwind-preset": "0.8.0-preview.a0fe3894",
  "@metamask-previews/design-system-twrnc-preset": "0.4.2-preview.a0fe3894",
  "@metamask-previews/design-tokens": "8.4.0-preview.a0fe3894"
}

Comment on lines +22 to +30
if (startAccessory !== null && startAccessory !== undefined) {
return startAccessory;
}

if (!severity || severity === ToastSeverity.Default) {
return undefined;
}

const { name, color } = TOAST_SEVERITY_ICON_MAP[severity];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That logic is so similar to what we have in mobile. Just thought that maybe this code can be unified somehow?

Comment on lines +28 to +33
if (!registeredRef?.current) {
const invocation = method === 'toast' ? 'toast()' : `toast.${method}()`;
throw new Error(
`${invocation} called before <Toaster /> mounted. Render <Toaster /> once at the root of your app.`,
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here, this code is copied/pasted. I think it's not blocking for this PR and we can merge it as is.

I'm just thinking whether it makes sense to create something like design-system-toaster/core or anything like that where we can put shared codebase for Toast?

Or you think that it may be an overkill, because we have different types anyway and we'll jsut get a more complex codebase because of that that will be harder to maintain?

}, timeoutDuration);
};

innerRef.current = { closeToast, showToast };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think assigning ref in the renderer will break react compiler rules. But I checked and the same code is present in react-native package too 🙈

I think it's fine for now, but we definitely need to make an audit (maybe add react compiler esling plugin) to spot all these violations automatically 🤞

Again, not blocking for these changes 👍

borderWidth={1}
className={twMerge('rounded-xl', className)}
closeButtonProps={resolvedCloseButtonProps}
onClose={onClose ? () => onClose() : undefined}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can this be simplifed to onClose={onClose}?

Theoretically it can be simplified to onClose={onClose ? onClose : undefined}, then to onClose={onClose ?? undefined}, but onClose already function or undefined, so theoretically it can be onClose={onClose}?

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.

Great catch! Missed this is self review thank you!


import { IconColor, IconName } from '../../types';

// TODO: Replace this map with a web IconAlert component once the shared

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just wanted to write a comment that we could unify the codebase 😁 But yes, your comment explains why we made it like this 👍

@kirillzyusko

Copy link
Copy Markdown
Collaborator

Also, do we need to prepare migration guide?

@georgewrmarshall georgewrmarshall enabled auto-merge (squash) June 4, 2026 19:40
@georgewrmarshall

Copy link
Copy Markdown
Contributor Author

Also, do we need to prepare migration guide?

Thanks for the thorough review @kirillzyusko! Will address your great suggestions in a follow up PR

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@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 prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Danger toast icon mismatches RN
    • Changed web Danger toast icon to IconName.Error to align with RN’s IconAlertSeverity.Error.

Create PR

Or push these changes by commenting:

@cursor push 4d3a6f56cd

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 920c661. Configure here.

[ToastSeverity.Danger]: {
name: IconName.Danger,
color: IconColor.ErrorDefault,
},

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.

Danger toast icon mismatches RN

Low Severity

For ToastSeverity.Danger, the web map uses IconName.Danger, while React Native maps danger to IconAlertSeverity.Error, which renders the error icon—so danger toasts can look different across platforms.

Fix in Cursor Fix in Web

Triggered by learned rule: Align component API shapes across web and RN platforms

Reviewed by Cursor Bugbot for commit 920c661. Configure here.

@georgewrmarshall georgewrmarshall merged commit c08280e into main Jun 4, 2026
37 checks passed
@georgewrmarshall georgewrmarshall deleted the toast-dsr branch June 4, 2026 19:44
georgewrmarshall added a commit that referenced this pull request Jun 9, 2026
## Release 44.0.0

This release adds `Toast`/`Toaster` and `Tag` to React, `ListItem` to
React Native, aligns the `TextButton` API across platforms, and
standardizes severity vocabulary (`Error` → `Danger`) across
`AvatarIcon`, `IconAlert`, and `Tag`.

### 📦 Package Versions

- `@metamask/design-system-shared`: **0.22.0**
- `@metamask/design-system-react`: **0.26.0**
- `@metamask/design-system-react-native`: **0.29.0**

### 🔄 Shared Type Updates (0.22.0)

#### New shared types (#1190, #1203, #1224, #1225)

**What Changed:**

- Added `ToastPropsShared` and `ToastSeverity` for cross-platform
`Toast` support
- Added `ListItemPropsShared` and related types for cross-platform
`ListItem` support
- Added `TextButtonPropsShared` to align `TextButton` API across React
and React Native
- Added `AvatarNetworkSize` as a named export from the shared package

**Impact:**

- Enables consistent `Toast` and `ListItem` implementations across both
platforms
- Continues ADR-0003/0004 const-object + string-union pattern adoption

#### Severity vocabulary: `.Error` → `.Danger`
([#1159](#1159))

**What Changed:**

- `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger`
- `IconAlertSeverity.Error` → `IconAlertSeverity.Danger`
- `TagSeverity.Error` → `TagSeverity.Danger`

**Impact:**

- Breaking change for any consumer using `.Error` on these three const
objects. Rendered colors are unchanged.

### 🌐 React Web Updates (0.26.0)

#### Added

- Added `Tag` component for categorization and filtering labels
([#1211](#1211))
- Added `Toast` component with `Toaster` provider and imperative
`toast()` API
([#1190](#1190))

#### Changed

- **BREAKING:** `TextButton` API aligned with React Native —
`size`/`TextButtonSize` replaced by `variant`/`TextVariant`;
`isInverse`, `isDisabled`, `textProps`, and icon/accessory props
removed; `asChild` added
([#1224](#1224))
- **BREAKING:** `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger`
([#1159](#1159))

#### Fixed

- Fixed `Toast` to support `toast()` calls made before `Toaster` mounts
([#1217](#1217))

### 📱 React Native Updates (0.29.0)

#### Added

- Added `ListItem` component for list row layouts
([#1203](#1203))
- Added `Toast` component with `Toaster` provider and imperative
`toast()` API
([#1190](#1190))

#### Changed

- **BREAKING:** `AvatarIconSeverity.Error`, `IconAlertSeverity.Error`,
and `TagSeverity.Error` → `.Danger`
([#1159](#1159))

### ⚠️ Breaking Changes

#### TextButton rewrite (React Web Only)

**What Changed:**

- `size` / `TextButtonSize` removed — use `variant` / `TextVariant`
instead
- `isInverse`, `isDisabled`, `textProps`, start/end icons, and accessory
slots removed
- `asChild` added for semantic link composition

**Migration:**

```tsx
// Before (0.25.0)
import { TextButton, TextButtonSize } from '@metamask/design-system-react';
<TextButton size={TextButtonSize.BodySm}>Learn more</TextButton>

// After (0.26.0)
import { TextButton } from '@metamask/design-system-react';
import { TextVariant } from '@metamask/design-system-shared';
<TextButton variant={TextVariant.BodySm}>Learn more</TextButton>
```

See [React Migration
Guide](./packages/design-system-react/MIGRATION.md#from-version-0250-to-0260)

#### Severity vocabulary: `.Error` → `.Danger` (Both Platforms)

**What Changed:**

- `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger` (React +
React Native)
- `IconAlertSeverity.Error` → `IconAlertSeverity.Danger` (React Native)
- `TagSeverity.Error` → `TagSeverity.Danger` (React Native)

**Migration:**

```tsx
// Before
<AvatarIcon severity={AvatarIconSeverity.Error} />
<IconAlert severity={IconAlertSeverity.Error} />
<Tag severity={TagSeverity.Error}>High risk</Tag>

// After
<AvatarIcon severity={AvatarIconSeverity.Danger} />
<IconAlert severity={IconAlertSeverity.Danger} />
<Tag severity={TagSeverity.Danger}>High risk</Tag>
```

See migration guides for complete instructions:

- [React Migration
Guide](./packages/design-system-react/MIGRATION.md#from-version-0250-to-0260)
- [React Native Migration
Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0280-to-0290)

### ✅ Checklist

- [x] Changelogs updated with human-readable descriptions
- [x] Changelog validation passed (`yarn changelog:validate`)
- [x] Version bumps follow semantic versioning
- [x] design-system-shared: minor (0.21.0 → 0.22.0) - new shared types +
breaking severity rename
- [x] design-system-react: minor (0.25.0 → 0.26.0) - new components +
breaking API changes
- [x] design-system-react-native: minor (0.28.0 → 0.29.0) - new
components + breaking severity rename
- [x] Breaking changes documented with migration guidance
- [x] Migration guides updated with before/after examples
- [x] PR references included in changelog entries

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs)
- [x] I've reviewed the [Release
Workflow](./.cursor/rules/release-workflow.md) cursor rule
- [ ] All tests pass (`yarn build && yarn test && yarn lint`)
- [x] Changelog validation passes (`yarn changelog:validate`)

## **Pre-merge reviewer checklist**

- [ ] I've reviewed the [Reviewing Release
PRs](./docs/reviewing-release-prs.md) guide
- [ ] Package versions follow semantic versioning
- [ ] Changelog entries are consumer-facing (not commit message
regurgitation)
- [ ] Breaking changes are documented in MIGRATION.md with examples
- [ ] All unreleased changes are accounted for in changelogs

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> The release documents multiple breaking consumer APIs (TextButton on
web, severity `.Error`→`.Danger`); upgrading without following
MIGRATION.md will cause compile/runtime mismatches, though this PR does
not change component implementation itself.
> 
> **Overview**
> **Release 44.0.0** cuts new versions of the design-system packages and
records what shipped since the last release: monorepo root **43.0.0 →
44.0.0**, `@metamask/design-system-shared` **0.22.0**,
`@metamask/design-system-react` **0.26.0**, and
`@metamask/design-system-react-native` **0.29.0**.
> 
> Changelogs document **React** additions (`Tag`,
`Toast`/`Toaster`/`toast()`), a **breaking** `TextButton` rewrite
(`size`/`TextButtonSize` → `variant`/`TextVariant`, dropped
inverse/disabled/icons, added `asChild`), and
**`AvatarIconSeverity.Error` → `.Danger`**. **React Native** adds
**`ListItem`** and the same **severity rename** on `AvatarIcon`,
`IconAlert`, and `Tag`. **Shared** adds cross-platform types
(`ListItem`, `Toast`, `TextButton`, `AvatarNetworkSize`) and the shared
**`.Error` → `.Danger`** severity vocabulary.
> 
> **MIGRATION.md** on React and React Native gains **0.25→0.26** and
**0.28→0.29** sections with before/after examples. The only non-release
code change in the diff is reordering the `react-native-worklets`
devDependency in `apps/storybook-react-native/package.json`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9c3345f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants