diff --git a/app/component-library/components/ButtonPrimary/index.ts b/app/component-library/components/ButtonPrimary/index.ts
index 94612932ed1..59027b5a078 100644
--- a/app/component-library/components/ButtonPrimary/index.ts
+++ b/app/component-library/components/ButtonPrimary/index.ts
@@ -1 +1,2 @@
export { default } from './ButtonPrimary';
+export { ButtonPrimaryVariant } from './ButtonPrimary.types';
diff --git a/app/component-library/components/ButtonSecondary/index.ts b/app/component-library/components/ButtonSecondary/index.ts
index 51b0e227aaa..4d54c5a57be 100644
--- a/app/component-library/components/ButtonSecondary/index.ts
+++ b/app/component-library/components/ButtonSecondary/index.ts
@@ -1 +1,2 @@
export { default } from './ButtonSecondary';
+export { ButtonSecondaryVariant } from './ButtonSecondary.types';
diff --git a/app/component-library/components/ButtonTertiary/index.ts b/app/component-library/components/ButtonTertiary/index.ts
index 8e60a7b1d5f..95ab2c9f70a 100644
--- a/app/component-library/components/ButtonTertiary/index.ts
+++ b/app/component-library/components/ButtonTertiary/index.ts
@@ -1 +1,2 @@
export { default } from './ButtonTertiary';
+export { ButtonTertiaryVariant } from './ButtonTertiary.types';
diff --git a/app/component-library/components/Toast/README.md b/app/component-library/components/Toast/README.md
new file mode 100644
index 00000000000..de3c7cfe626
--- /dev/null
+++ b/app/component-library/components/Toast/README.md
@@ -0,0 +1,80 @@
+# Toast
+
+Toast is a component that slides up from the bottom. It is typically used to show post confirmation information.
+
+## Methods
+
+### `showToast()`
+
+```javascript
+showToast(toastOptions: ToastOptions)
+```
+
+| PARAMETERS | TYPE | DESCRIPTION |
+| :-------------------------------------------------------- | :-------------------------------------------------- | :--------------------------------------------------------- |
+| toastOptions | [ToastOptions](./Toast.types.ts#L36) | Toast options to show. |
+
+## Use Case
+
+Using this component requires a three step process:
+
+1. Wrap a root element with `ToastContextWrapper`.
+
+```javascript
+// Replace import with relative path.
+import { ToastContextWrapper } from 'app/component-library/components/Toast';
+
+const App = () => (
+
+
+
+);
+```
+
+2. Implement `Toast` component within a child of the root element and apply `toastRef` from `ToastContext`.
+
+```javascript
+// Replace import with relative path.
+import Toast, { ToastContext } from 'app/component-library/components/Toast';
+
+const Root = () => {
+ const { toastRef } = useContext(ToastContext);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+```
+
+3. Reference `toastRef` and call `toastRef.current?.showToast(...)` to show toast.
+
+```javascript
+// Replace import with relative path.
+import Toast, { ToastContext } from 'app/component-library/components/Toast';
+
+const NestedComponent = () => {
+ const { toastRef } = useContext(ToastContext);
+
+ const showToast = () => {
+ // Example of showing toast with Account variant.
+ toastRef.current?.showToast({
+ variant: ToastVariant.Account,
+ labelOptions: [
+ { label: 'Switching to' },
+ { label: ' Account 2.', isBold: true },
+ ],
+ accountAddress:
+ '0x10e08af911f2e489480fb2855b24771745d0198b50f5c55891369844a8c57092',
+ linkOption: {
+ label: 'Click here.',
+ onPress: () => {},
+ },
+ });
+ };
+
+ return ;
+};
+```
diff --git a/app/component-library/components/Toast/Toast.context.tsx b/app/component-library/components/Toast/Toast.context.tsx
new file mode 100644
index 00000000000..7fa9bd863a7
--- /dev/null
+++ b/app/component-library/components/Toast/Toast.context.tsx
@@ -0,0 +1,16 @@
+/* eslint-disable react/prop-types */
+import React, { useRef } from 'react';
+import { ToastRef, ToastContextParams } from './Toast.types';
+
+export const ToastContext = React.createContext({
+ toastRef: undefined,
+});
+
+export const ToastContextWrapper: React.FC = ({ children }) => {
+ const toastRef = useRef(null);
+ return (
+
+ {children}
+
+ );
+};
diff --git a/app/component-library/components/Toast/Toast.stories.tsx b/app/component-library/components/Toast/Toast.stories.tsx
new file mode 100644
index 00000000000..81cd579e265
--- /dev/null
+++ b/app/component-library/components/Toast/Toast.stories.tsx
@@ -0,0 +1,79 @@
+/* eslint-disable no-console */
+import React, { useContext } from 'react';
+import { Alert } from 'react-native';
+import { storiesOf } from '@storybook/react-native';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+import { ToastVariant } from './Toast.types';
+import { BaseButtonSize } from '../BaseButton';
+import ButtonTertiary, { ButtonTertiaryVariant } from '../ButtonTertiary';
+
+const ToastExample = () => {
+ const { toastRef } = useContext(ToastContext);
+
+ return (
+ <>
+ {
+ toastRef?.current?.showToast({
+ variant: ToastVariant.Account,
+ labelOptions: [
+ { label: 'Switching to' },
+ { label: ' Account 2.', isBold: true },
+ ],
+ accountAddress:
+ '0x10e08af911f2e489480fb2855b24771745d0198b50f5c55891369844a8c57092',
+ });
+ }}
+ />
+ {
+ toastRef?.current?.showToast({
+ variant: ToastVariant.Network,
+ labelOptions: [
+ { label: 'Added' },
+ { label: ' Mainnet', isBold: true },
+ { label: ' network.' },
+ ],
+ networkImageUrl:
+ 'https://assets.coingecko.com/coins/images/279/small/ethereum.png?1595348880',
+ linkOption: {
+ label: 'Click here!',
+ onPress: () => {
+ Alert.alert('Clicked toast link!');
+ },
+ },
+ });
+ }}
+ />
+ {
+ toastRef?.current?.showToast({
+ variant: ToastVariant.Plain,
+ labelOptions: [{ label: 'This is a plain message.' }],
+ });
+ }}
+ />
+
+
+ >
+ );
+};
+
+storiesOf('Component Library / Toast', module)
+ .addDecorator((storyFn) => (
+
+ {storyFn()}
+
+ ))
+ .add('Default', () => );
diff --git a/app/component-library/components/Toast/Toast.styles.ts b/app/component-library/components/Toast/Toast.styles.ts
new file mode 100644
index 00000000000..96cd29b770f
--- /dev/null
+++ b/app/component-library/components/Toast/Toast.styles.ts
@@ -0,0 +1,36 @@
+import { StyleSheet, Dimensions } from 'react-native';
+import { darkTheme } from '@metamask/design-tokens';
+
+const { colors } = darkTheme;
+const marginWidth = 16;
+const padding = 16;
+const toastWidth = Dimensions.get('window').width - marginWidth * 2;
+
+/**
+ * Style sheet for Toast component.
+ *
+ * @returns StyleSheet object.
+ */
+const styleSheet = StyleSheet.create({
+ base: {
+ position: 'absolute',
+ width: toastWidth,
+ left: marginWidth,
+ bottom: 0,
+ backgroundColor: colors.background.alternative,
+ borderRadius: 4,
+ padding,
+ flexDirection: 'row',
+ },
+ avatar: {
+ marginRight: 8,
+ },
+ labelsContainer: {
+ justifyContent: 'center',
+ },
+ label: {
+ color: colors.text.default,
+ },
+});
+
+export default styleSheet;
diff --git a/app/component-library/components/Toast/Toast.tsx b/app/component-library/components/Toast/Toast.tsx
new file mode 100644
index 00000000000..275b51ed72d
--- /dev/null
+++ b/app/component-library/components/Toast/Toast.tsx
@@ -0,0 +1,171 @@
+/* eslint-disable react/prop-types */
+import React, {
+ forwardRef,
+ useImperativeHandle,
+ useMemo,
+ useState,
+} from 'react';
+import {
+ Dimensions,
+ LayoutChangeEvent,
+ StyleProp,
+ View,
+ ViewStyle,
+} from 'react-native';
+import Animated, {
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withDelay,
+ withTiming,
+} from 'react-native-reanimated';
+import AccountAvatar, { AccountAvatarType } from '../AccountAvatar';
+import { BaseAvatarSize } from '../BaseAvatar';
+import BaseText, { BaseTextVariant } from '../BaseText';
+import Link from '../Link';
+import styles from './Toast.styles';
+import {
+ ToastLabelOptions,
+ ToastLinkOption,
+ ToastOptions,
+ ToastRef,
+ ToastVariant,
+} from './Toast.types';
+import NetworkAvatar from '../NetworkAvatar';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const visibilityDuration = 2500;
+const animationDuration = 250;
+const bottomPadding = 16;
+const screenHeight = Dimensions.get('window').height;
+
+const Toast = forwardRef((_, ref: React.ForwardedRef) => {
+ const [toastOptions, setToastOptions] = useState(
+ undefined,
+ );
+ const { bottom: bottomNotchSpacing } = useSafeAreaInsets();
+ const translateYProgress = useSharedValue(screenHeight);
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ translateY: translateYProgress.value }],
+ }));
+ const baseStyle: StyleProp>> =
+ useMemo(
+ () => [styles.base, animatedStyle],
+ /* eslint-disable-next-line */
+ [],
+ );
+
+ const showToast = (options: ToastOptions) => {
+ if (toastOptions) {
+ return;
+ }
+ setToastOptions(options);
+ };
+
+ useImperativeHandle(ref, () => ({
+ showToast,
+ }));
+
+ const resetState = () => setToastOptions(undefined);
+
+ const onAnimatedViewLayout = (e: LayoutChangeEvent) => {
+ if (toastOptions) {
+ const { height } = e.nativeEvent.layout;
+ const translateYToValue = -(bottomPadding + bottomNotchSpacing);
+
+ translateYProgress.value = height;
+ translateYProgress.value = withTiming(
+ translateYToValue,
+ { duration: animationDuration },
+ () =>
+ (translateYProgress.value = withDelay(
+ visibilityDuration,
+ withTiming(
+ height,
+ { duration: animationDuration },
+ runOnJS(resetState),
+ ),
+ )),
+ );
+ }
+ };
+
+ const renderLabel = (labelOptions: ToastLabelOptions) => (
+
+ {labelOptions.map(({ label, isBold }, index) => (
+
+ {label}
+
+ ))}
+
+ );
+
+ const renderLink = (linkOption?: ToastLinkOption) =>
+ linkOption ? (
+
+ {linkOption.label}
+
+ ) : null;
+
+ const renderAvatar = () => {
+ switch (toastOptions?.variant) {
+ case ToastVariant.Plain:
+ return null;
+ case ToastVariant.Account: {
+ const { accountAddress } = toastOptions;
+ return (
+
+ );
+ }
+ case ToastVariant.Network: {
+ {
+ const { networkImageUrl } = toastOptions;
+ return (
+
+ );
+ }
+ }
+ }
+ };
+
+ const renderToastContent = (options: ToastOptions) => {
+ const { labelOptions, linkOption } = options;
+
+ return (
+ <>
+ {renderAvatar()}
+
+ {renderLabel(labelOptions)}
+ {renderLink(linkOption)}
+
+ >
+ );
+ };
+
+ if (!toastOptions) {
+ return null;
+ }
+
+ return (
+
+ {renderToastContent(toastOptions)}
+
+ );
+});
+
+export default Toast;
diff --git a/app/component-library/components/Toast/Toast.types.ts b/app/component-library/components/Toast/Toast.types.ts
new file mode 100644
index 00000000000..efdd57049b7
--- /dev/null
+++ b/app/component-library/components/Toast/Toast.types.ts
@@ -0,0 +1,47 @@
+export enum ToastVariant {
+ Plain = 'Plain',
+ Account = 'Account',
+ Network = 'Network',
+}
+
+export type ToastLabelOptions = {
+ label: string;
+ isBold?: boolean;
+}[];
+
+export interface ToastLinkOption {
+ label: string;
+ onPress: () => void;
+}
+
+interface BaseToastVariant {
+ labelOptions: ToastLabelOptions;
+ linkOption?: ToastLinkOption;
+}
+
+interface PlainToastOption extends BaseToastVariant {
+ variant: ToastVariant.Plain;
+}
+
+interface AccountToastOption extends BaseToastVariant {
+ variant: ToastVariant.Account;
+ accountAddress: string;
+}
+
+interface NetworkToastOption extends BaseToastVariant {
+ variant: ToastVariant.Network;
+ networkImageUrl: string;
+}
+
+export type ToastOptions =
+ | PlainToastOption
+ | AccountToastOption
+ | NetworkToastOption;
+
+export interface ToastRef {
+ showToast: (toastOptions: ToastOptions) => void;
+}
+
+export interface ToastContextParams {
+ toastRef: React.RefObject | undefined;
+}
diff --git a/app/component-library/components/Toast/index.ts b/app/component-library/components/Toast/index.ts
new file mode 100644
index 00000000000..242ffd87421
--- /dev/null
+++ b/app/component-library/components/Toast/index.ts
@@ -0,0 +1,3 @@
+export { default } from './Toast';
+export { ToastVariant } from './Toast.types';
+export { ToastContext, ToastContextWrapper } from './Toast.context';
diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index 8a147a4b22a..6f8658cacfd 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -1,4 +1,10 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
import { NavigationContainer, CommonActions } from '@react-navigation/native';
import { Animated, Linking } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
@@ -44,6 +50,9 @@ import { mockTheme, useAppThemeFromContext } from '../../../util/theme';
import Device from '../../../util/device';
import { colors as importedColors } from '../../../styles/common';
import Routes from '../../../constants/navigation/Routes';
+import Toast, {
+ ToastContext,
+} from '../../../component-library/components/Toast';
const Stack = createStackNavigator();
/**
@@ -149,6 +158,7 @@ const App = ({ userLoggedIn }) => {
const [route, setRoute] = useState();
const [animationPlayed, setAnimationPlayed] = useState();
const { colors } = useAppThemeFromContext() || mockTheme;
+ const { toastRef } = useContext(ToastContext);
const isAuthChecked = useSelector((state) => state.user.isAuthChecked);
const dispatch = useDispatch();
@@ -386,6 +396,7 @@ const App = ({ userLoggedIn }) => {
{renderSplash()}
+
>
)) ||
null
diff --git a/app/components/Views/Root/index.js b/app/components/Views/Root/index.js
index 254d52faa7b..d36cebae9ba 100644
--- a/app/components/Views/Root/index.js
+++ b/app/components/Views/Root/index.js
@@ -10,6 +10,8 @@ import EntryScriptWeb3 from '../../../core/EntryScriptWeb3';
import Logger from '../../../util/Logger';
import ErrorBoundary from '../ErrorBoundary';
import { useAppTheme, ThemeContext } from '../../../util/theme';
+import { ToastContextWrapper } from '../../../component-library/components/Toast';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
/**
* Top level of the component hierarchy
@@ -52,10 +54,14 @@ const ConnectedRoot = () => {
const theme = useAppTheme();
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
};
diff --git a/storybook/storyLoader.js b/storybook/storyLoader.js
index b5b2b1b56b7..cac8ec221d5 100644
--- a/storybook/storyLoader.js
+++ b/storybook/storyLoader.js
@@ -34,6 +34,7 @@ function loadStories() {
require('../app/component-library/components/TabBarItem/TabBarItem.stories');
require('../app/component-library/components/Tag/Tag.stories');
require('../app/component-library/components/TagUrl/TagUrl.stories');
+ require('../app/component-library/components/Toast/Toast.stories');
require('../app/component-library/components/TokenAvatar/TokenAvatar.stories');
}
@@ -68,6 +69,7 @@ const stories = [
'../app/component-library/components/TabBarItem/TabBarItem.stories',
'../app/component-library/components/Tag/Tag.stories',
'../app/component-library/components/TagUrl/TagUrl.stories',
+ '../app/component-library/components/Toast/Toast.stories',
'../app/component-library/components/TokenAvatar/TokenAvatar.stories',
];