diff --git a/.eslintrc.js b/.eslintrc.js
index 179b5859e59..e989083566b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -909,5 +909,10 @@ module.exports = {
'no-loss-of-precision': 'off',
},
- ignorePatterns: ['wdio.conf.js', 'app/util/termsOfUse/termsOfUseContent.ts'],
+ ignorePatterns: [
+ 'wdio.conf.js',
+ 'app/util/termsOfUse/termsOfUseContent.ts',
+ '!.storybook',
+ '!.storybook/**',
+ ],
};
diff --git a/.storybook/index.js b/.storybook/index.js
index e8773f90e69..d4ec36674f6 100644
--- a/.storybook/index.js
+++ b/.storybook/index.js
@@ -1,4 +1,6 @@
+import React, { useEffect } from 'react';
import { getStorybookUI } from '@storybook/react-native';
+import { hideAsync } from 'expo-splash-screen';
import './storybook.requires';
@@ -6,4 +8,14 @@ const StorybookUIRoot = getStorybookUI({
asyncStorage: null,
});
-export { StorybookUIRoot as default };
+function StorybookRoot() {
+ useEffect(() => {
+ hideAsync().catch(() => {
+ // Non-fatal — Storybook can still render if splash hide fails
+ });
+ }, []);
+
+ return ;
+}
+
+export { StorybookRoot as default };
diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js
index 661188f3376..679e927255d 100644
--- a/.storybook/storybook.requires.js
+++ b/.storybook/storybook.requires.js
@@ -6,27 +6,27 @@ import {
addParameters,
addArgsEnhancer,
clearDecorators,
-} from "@storybook/react-native";
+} from '@storybook/react-native';
global.STORIES = [
{
- titlePrefix: "",
- directory: "./app",
- files: "**/*.stories.?(ts|tsx|js|jsx)",
+ titlePrefix: '',
+ directory: './app',
+ files: '**/*.stories.?(ts|tsx|js|jsx)',
importPathMatcher:
- "^\\.[\\\\/](?:app(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)\\/|\\/|$)(?!\\.)(?=.)[^/]*?\\.stories\\.(?:ts|tsx|js|jsx)?)$",
+ '^\\.[\\\\/](?:app(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)\\/|\\/|$)(?!\\.)(?=.)[^/]*?\\.stories\\.(?:ts|tsx|js|jsx)?)$',
},
];
-import "@storybook/addon-ondevice-controls/register";
+import '@storybook/addon-ondevice-controls/register';
-import { decorators, parameters } from "./preview";
+import { decorators, parameters } from './preview';
if (decorators) {
if (__DEV__) {
// stops the warning from showing on every HMR
- require("react-native").LogBox.ignoreLogs([
- "`clearDecorators` is deprecated and will be removed in Storybook 7.0",
+ require('react-native').LogBox.ignoreLogs([
+ '`clearDecorators` is deprecated and will be removed in Storybook 7.0',
]);
}
// workaround for global decorators getting infinitely applied on HMR, see https://github.com/storybookjs/react-native/issues/185
@@ -38,116 +38,126 @@ if (parameters) {
addParameters(parameters);
}
-const getStories = () => {
- return {
- "./app/component-library/base-components/TagBase/TagBase.stories.tsx": require("../app/component-library/base-components/TagBase/TagBase.stories.tsx"),
- "./app/component-library/components-temp/ActionListItem/ActionListItem.stories.tsx": require("../app/component-library/components-temp/ActionListItem/ActionListItem.stories.tsx"),
- "./app/component-library/components-temp/ButtonFilter/ButtonFilter.stories.tsx": require("../app/component-library/components-temp/ButtonFilter/ButtonFilter.stories.tsx"),
- "./app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.stories.tsx": require("../app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.stories.tsx"),
- "./app/component-library/components-temp/Buttons/ButtonSemantic/ButtonSemantic.stories.tsx": require("../app/component-library/components-temp/Buttons/ButtonSemantic/ButtonSemantic.stories.tsx"),
- "./app/component-library/components-temp/Buttons/ButtonToggle/ButtonToggle.stories.tsx": require("../app/component-library/components-temp/Buttons/ButtonToggle/ButtonToggle.stories.tsx"),
- "./app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx": require("../app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx"),
- "./app/component-library/components-temp/HeaderCompactStandard/HeaderCompactStandard.stories.tsx": require("../app/component-library/components-temp/HeaderCompactStandard/HeaderCompactStandard.stories.tsx"),
- "./app/component-library/components-temp/HeaderRoot/HeaderRoot.stories.tsx": require("../app/component-library/components-temp/HeaderRoot/HeaderRoot.stories.tsx"),
- "./app/component-library/components-temp/HeaderSearch/HeaderSearch.stories.tsx": require("../app/component-library/components-temp/HeaderSearch/HeaderSearch.stories.tsx"),
- "./app/component-library/components-temp/HeaderStandardAnimated/HeaderStandardAnimated.stories.tsx": require("../app/component-library/components-temp/HeaderStandardAnimated/HeaderStandardAnimated.stories.tsx"),
- "./app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx": require("../app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx"),
- "./app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx": require("../app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx"),
- "./app/component-library/components-temp/ListItemMultiSelectWithMenuButton/ListItemMultiSelectWithMenuButton.stories.tsx": require("../app/component-library/components-temp/ListItemMultiSelectWithMenuButton/ListItemMultiSelectWithMenuButton.stories.tsx"),
- "./app/component-library/components-temp/MainActionButton/MainActionButton.stories.tsx": require("../app/component-library/components-temp/MainActionButton/MainActionButton.stories.tsx"),
- "./app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.stories.tsx"),
- "./app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.stories.tsx"),
- "./app/component-library/components-temp/MultichainAccounts/MultichainAddressRow/MultichainAddressRow.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/MultichainAddressRow/MultichainAddressRow.stories.tsx"),
- "./app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.stories.tsx"),
- "./app/component-library/components-temp/Pressable/Pressable.stories.tsx": require("../app/component-library/components-temp/Pressable/Pressable.stories.tsx"),
- "./app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx": require("../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx"),
- "./app/component-library/components-temp/SectionHeader/SectionHeader.stories.tsx": require("../app/component-library/components-temp/SectionHeader/SectionHeader.stories.tsx"),
- "./app/component-library/components-temp/TabBar/TabBar.stories.tsx": require("../app/component-library/components-temp/TabBar/TabBar.stories.tsx"),
- "./app/component-library/components-temp/TabEmptyState/TabEmptyState.stories.tsx": require("../app/component-library/components-temp/TabEmptyState/TabEmptyState.stories.tsx"),
- "./app/component-library/components-temp/Tabs/Tabs.stories.tsx": require("../app/component-library/components-temp/Tabs/Tabs.stories.tsx"),
- "./app/component-library/components-temp/TagColored/TagColored.stories.tsx": require("../app/component-library/components-temp/TagColored/TagColored.stories.tsx"),
- "./app/component-library/components-temp/TitleStandard/TitleStandard.stories.tsx": require("../app/component-library/components-temp/TitleStandard/TitleStandard.stories.tsx"),
- "./app/component-library/components-temp/TitleSubpage/TitleSubpage.stories.tsx": require("../app/component-library/components-temp/TitleSubpage/TitleSubpage.stories.tsx"),
- "./app/component-library/components/Accordions/Accordion/Accordion.stories.tsx": require("../app/component-library/components/Accordions/Accordion/Accordion.stories.tsx"),
- "./app/component-library/components/Accordions/Accordion/foundation/AccordionHeader/AccordionHeader.stories.tsx": require("../app/component-library/components/Accordions/Accordion/foundation/AccordionHeader/AccordionHeader.stories.tsx"),
- "./app/component-library/components/Avatars/Avatar/Avatar.stories.tsx": require("../app/component-library/components/Avatars/Avatar/Avatar.stories.tsx"),
- "./app/component-library/components/Avatars/Avatar/variants/AvatarAccount/AvatarAccount.stories.tsx": require("../app/component-library/components/Avatars/Avatar/variants/AvatarAccount/AvatarAccount.stories.tsx"),
- "./app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.stories.tsx": require("../app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.stories.tsx"),
- "./app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.stories.tsx": require("../app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.stories.tsx"),
- "./app/component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.stories.tsx": require("../app/component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.stories.tsx"),
- "./app/component-library/components/Avatars/Avatar/variants/AvatarToken/AvatarToken.stories.tsx": require("../app/component-library/components/Avatars/Avatar/variants/AvatarToken/AvatarToken.stories.tsx"),
- "./app/component-library/components/Avatars/AvatarGroup/AvatarGroup.stories.tsx": require("../app/component-library/components/Avatars/AvatarGroup/AvatarGroup.stories.tsx"),
- "./app/component-library/components/Badges/Badge/Badge.stories.tsx": require("../app/component-library/components/Badges/Badge/Badge.stories.tsx"),
- "./app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx": require("../app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx"),
- "./app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx": require("../app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx"),
- "./app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.stories.tsx": require("../app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.stories.tsx"),
- "./app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.stories.tsx": require("../app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.stories.tsx"),
- "./app/component-library/components/Banners/Banner/Banner.stories.tsx": require("../app/component-library/components/Banners/Banner/Banner.stories.tsx"),
- "./app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories.tsx": require("../app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories.tsx"),
- "./app/component-library/components/Banners/Banner/variants/BannerTip/BannerTip.stories.tsx": require("../app/component-library/components/Banners/Banner/variants/BannerTip/BannerTip.stories.tsx"),
- "./app/component-library/components/BottomSheets/BottomSheet/BottomSheet.stories.tsx": require("../app/component-library/components/BottomSheets/BottomSheet/BottomSheet.stories.tsx"),
- "./app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx": require("../app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx"),
- "./app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx": require("../app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx"),
- "./app/component-library/components/Buttons/Button/Button.stories.tsx": require("../app/component-library/components/Buttons/Button/Button.stories.tsx"),
- "./app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.stories.tsx": require("../app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.stories.tsx"),
- "./app/component-library/components/Buttons/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx": require("../app/component-library/components/Buttons/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx"),
- "./app/component-library/components/Buttons/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx": require("../app/component-library/components/Buttons/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx"),
- "./app/component-library/components/Buttons/ButtonIcon/ButtonIcon.stories.tsx": require("../app/component-library/components/Buttons/ButtonIcon/ButtonIcon.stories.tsx"),
- "./app/component-library/components/Cards/Card/Card.stories.tsx": require("../app/component-library/components/Cards/Card/Card.stories.tsx"),
- "./app/component-library/components/Cells/Cell/Cell.stories.tsx": require("../app/component-library/components/Cells/Cell/Cell.stories.tsx"),
- "./app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.stories.tsx": require("../app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.stories.tsx"),
- "./app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.stories.tsx": require("../app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.stories.tsx"),
- "./app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.stories.tsx": require("../app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.stories.tsx"),
- "./app/component-library/components/Checkbox/Checkbox.stories.tsx": require("../app/component-library/components/Checkbox/Checkbox.stories.tsx"),
- "./app/component-library/components/design-system.stories.tsx": require("../app/component-library/components/design-system.stories.tsx"),
- "./app/component-library/components/Form/HelpText/HelpText.stories.tsx": require("../app/component-library/components/Form/HelpText/HelpText.stories.tsx"),
- "./app/component-library/components/Form/Label/Label.stories.tsx": require("../app/component-library/components/Form/Label/Label.stories.tsx"),
- "./app/component-library/components/Form/TextField/foundation/Input/Input.stories.tsx": require("../app/component-library/components/Form/TextField/foundation/Input/Input.stories.tsx"),
- "./app/component-library/components/Form/TextField/TextField.stories.tsx": require("../app/component-library/components/Form/TextField/TextField.stories.tsx"),
- "./app/component-library/components/Form/TextFieldSearch/TextFieldSearch.stories.tsx": require("../app/component-library/components/Form/TextFieldSearch/TextFieldSearch.stories.tsx"),
- "./app/component-library/components/HeaderBase/HeaderBase.stories.tsx": require("../app/component-library/components/HeaderBase/HeaderBase.stories.tsx"),
- "./app/component-library/components/Icons/Icon/Icon.stories.tsx": require("../app/component-library/components/Icons/Icon/Icon.stories.tsx"),
- "./app/component-library/components/List/ListItem/ListItem.stories.tsx": require("../app/component-library/components/List/ListItem/ListItem.stories.tsx"),
- "./app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.stories.tsx": require("../app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.stories.tsx"),
- "./app/component-library/components/List/ListItemSelect/ListItemSelect.stories.tsx": require("../app/component-library/components/List/ListItemSelect/ListItemSelect.stories.tsx"),
- "./app/component-library/components/Modals/ModalConfirmation/ModalConfirmation.stories.tsx": require("../app/component-library/components/Modals/ModalConfirmation/ModalConfirmation.stories.tsx"),
- "./app/component-library/components/Modals/ModalMandatory/ModalMandatory.stories.tsx": require("../app/component-library/components/Modals/ModalMandatory/ModalMandatory.stories.tsx"),
- "./app/component-library/components/Navigation/TabBar/TabBar.stories.tsx": require("../app/component-library/components/Navigation/TabBar/TabBar.stories.tsx"),
- "./app/component-library/components/Navigation/TabBarItem/TabBarItem.stories.tsx": require("../app/component-library/components/Navigation/TabBarItem/TabBarItem.stories.tsx"),
- "./app/component-library/components/Overlay/Overlay.stories.tsx": require("../app/component-library/components/Overlay/Overlay.stories.tsx"),
- "./app/component-library/components/Pickers/PickerAccount/PickerAccount.stories.tsx": require("../app/component-library/components/Pickers/PickerAccount/PickerAccount.stories.tsx"),
- "./app/component-library/components/Pickers/PickerBase/PickerBase.stories.tsx": require("../app/component-library/components/Pickers/PickerBase/PickerBase.stories.tsx"),
- "./app/component-library/components/Pickers/PickerNetwork/PickerNetwork.stories.tsx": require("../app/component-library/components/Pickers/PickerNetwork/PickerNetwork.stories.tsx"),
- "./app/component-library/components/RadioButton/RadioButton.stories.tsx": require("../app/component-library/components/RadioButton/RadioButton.stories.tsx"),
- "./app/component-library/components/Select/SelectButton/SelectButton.stories.tsx": require("../app/component-library/components/Select/SelectButton/SelectButton.stories.tsx"),
- "./app/component-library/components/Select/SelectOption/SelectOption.stories.tsx": require("../app/component-library/components/Select/SelectOption/SelectOption.stories.tsx"),
- "./app/component-library/components/Select/SelectValue/SelectValue.stories.tsx": require("../app/component-library/components/Select/SelectValue/SelectValue.stories.tsx"),
- "./app/component-library/components/Sheet/SheetHeader/SheetHeader.stories.tsx": require("../app/component-library/components/Sheet/SheetHeader/SheetHeader.stories.tsx"),
- "./app/component-library/components/Skeleton/Skeleton.stories.tsx": require("../app/component-library/components/Skeleton/Skeleton.stories.tsx"),
- "./app/component-library/components/Tags/Tag/Tag.stories.tsx": require("../app/component-library/components/Tags/Tag/Tag.stories.tsx"),
- "./app/component-library/components/Tags/TagUrl/TagUrl.stories.tsx": require("../app/component-library/components/Tags/TagUrl/TagUrl.stories.tsx"),
- "./app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx": require("../app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx"),
- "./app/component-library/components/Texts/Text/Text.stories.tsx": require("../app/component-library/components/Texts/Text/Text.stories.tsx"),
- "./app/component-library/components/Texts/TextWithPrefixIcon/TextWithPrefixIcon.stories.tsx": require("../app/component-library/components/Texts/TextWithPrefixIcon/TextWithPrefixIcon.stories.tsx"),
- "./app/component-library/components/Toast/Toast.stories.tsx": require("../app/component-library/components/Toast/Toast.stories.tsx"),
- "./app/components/Base/Keypad/Keypad.stories.tsx": require("../app/components/Base/Keypad/Keypad.stories.tsx"),
- "./app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx": require("../app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx"),
- "./app/components/UI/CollectiblesEmptyState/CollectiblesEmptyState.stories.tsx": require("../app/components/UI/CollectiblesEmptyState/CollectiblesEmptyState.stories.tsx"),
- "./app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx": require("../app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx"),
- "./app/components/UI/Perps/Views/PerpsEmptyState/PerpsEmptyState.stories.tsx": require("../app/components/UI/Perps/Views/PerpsEmptyState/PerpsEmptyState.stories.tsx"),
- "./app/components/UI/Ramp/Aggregator/components/ShapesBackgroundAnimation/index.stories.tsx": require("../app/components/UI/Ramp/Aggregator/components/ShapesBackgroundAnimation/index.stories.tsx"),
- "./app/components/UI/Rewards/components/RewardPointsAnimation/RewardPointsAnimation.stories.tsx": require("../app/components/UI/Rewards/components/RewardPointsAnimation/RewardPointsAnimation.stories.tsx"),
- "./app/components/UI/SettingsButtonSection/SettingsButtonSection.stories.tsx": require("../app/components/UI/SettingsButtonSection/SettingsButtonSection.stories.tsx"),
- "./app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx": require("../app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx"),
- "./app/components/Views/confirmations/components/deposit-keyboard/deposit-keyboard.stories.tsx": require("../app/components/Views/confirmations/components/deposit-keyboard/deposit-keyboard.stories.tsx"),
- "./app/components/Views/confirmations/components/edit-amount-keyboard/edit-amount-keyboard.stories.tsx": require("../app/components/Views/confirmations/components/edit-amount-keyboard/edit-amount-keyboard.stories.tsx"),
- "./app/components/Views/ConnectHardware/HardwareWalletDiscoveryFlow/screens/errors/index.stories.tsx": require("../app/components/Views/ConnectHardware/HardwareWalletDiscoveryFlow/screens/errors/index.stories.tsx"),
- "./app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.stories.tsx": require("../app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.stories.tsx"),
- "./app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.stories.tsx": require("../app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.stories.tsx"),
- "./app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.stories.tsx": require("../app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.stories.tsx"),
- "./app/components/Views/MultichainAccounts/MultichainPermissionsSummary/MultichainPermissionsSummary.stories.tsx": require("../app/components/Views/MultichainAccounts/MultichainPermissionsSummary/MultichainPermissionsSummary.stories.tsx"),
- "./app/components/Views/QRAccountDisplay/QRAccountDisplay.stories.tsx": require("../app/components/Views/QRAccountDisplay/QRAccountDisplay.stories.tsx"),
- };
-};
+const getStories = () => ({
+ './app/component-library/base-components/TagBase/TagBase.stories.tsx': require('../app/component-library/base-components/TagBase/TagBase.stories.tsx'),
+ './app/component-library/components-temp/ActionListItem/ActionListItem.stories.tsx': require('../app/component-library/components-temp/ActionListItem/ActionListItem.stories.tsx'),
+ './app/component-library/components-temp/BaseNotification/BaseNotification.stories.tsx': require('../app/component-library/components-temp/BaseNotification/BaseNotification.stories.tsx'),
+ './app/component-library/components-temp/ButtonFilter/ButtonFilter.stories.tsx': require('../app/component-library/components-temp/ButtonFilter/ButtonFilter.stories.tsx'),
+ './app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.stories.tsx': require('../app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.stories.tsx'),
+ './app/component-library/components-temp/Buttons/ButtonSemantic/ButtonSemantic.stories.tsx': require('../app/component-library/components-temp/Buttons/ButtonSemantic/ButtonSemantic.stories.tsx'),
+ './app/component-library/components-temp/Buttons/ButtonToggle/ButtonToggle.stories.tsx': require('../app/component-library/components-temp/Buttons/ButtonToggle/ButtonToggle.stories.tsx'),
+ './app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx': require('../app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx'),
+ './app/component-library/components-temp/HeaderCompactStandard/HeaderCompactStandard.stories.tsx': require('../app/component-library/components-temp/HeaderCompactStandard/HeaderCompactStandard.stories.tsx'),
+ './app/component-library/components-temp/HeaderRoot/HeaderRoot.stories.tsx': require('../app/component-library/components-temp/HeaderRoot/HeaderRoot.stories.tsx'),
+ './app/component-library/components-temp/HeaderSearch/HeaderSearch.stories.tsx': require('../app/component-library/components-temp/HeaderSearch/HeaderSearch.stories.tsx'),
+ './app/component-library/components-temp/HeaderStandardAnimated/HeaderStandardAnimated.stories.tsx': require('../app/component-library/components-temp/HeaderStandardAnimated/HeaderStandardAnimated.stories.tsx'),
+ './app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx': require('../app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx'),
+ './app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx': require('../app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx'),
+ './app/component-library/components-temp/ListItemMultiSelectWithMenuButton/ListItemMultiSelectWithMenuButton.stories.tsx': require('../app/component-library/components-temp/ListItemMultiSelectWithMenuButton/ListItemMultiSelectWithMenuButton.stories.tsx'),
+ './app/component-library/components-temp/MainActionButton/MainActionButton.stories.tsx': require('../app/component-library/components-temp/MainActionButton/MainActionButton.stories.tsx'),
+ './app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.stories.tsx': require('../app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.stories.tsx'),
+ './app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.stories.tsx': require('../app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.stories.tsx'),
+ './app/component-library/components-temp/MultichainAccounts/MultichainAddressRow/MultichainAddressRow.stories.tsx': require('../app/component-library/components-temp/MultichainAccounts/MultichainAddressRow/MultichainAddressRow.stories.tsx'),
+ './app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.stories.tsx': require('../app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.stories.tsx'),
+ './app/component-library/components-temp/Pressable/Pressable.stories.tsx': require('../app/component-library/components-temp/Pressable/Pressable.stories.tsx'),
+ './app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx': require('../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx'),
+ './app/component-library/components-temp/SectionHeader/SectionHeader.stories.tsx': require('../app/component-library/components-temp/SectionHeader/SectionHeader.stories.tsx'),
+ './app/component-library/components-temp/TabBar/TabBar.stories.tsx': require('../app/component-library/components-temp/TabBar/TabBar.stories.tsx'),
+ './app/component-library/components-temp/TabEmptyState/TabEmptyState.stories.tsx': require('../app/component-library/components-temp/TabEmptyState/TabEmptyState.stories.tsx'),
+ './app/component-library/components-temp/Tabs/Tabs.stories.tsx': require('../app/component-library/components-temp/Tabs/Tabs.stories.tsx'),
+ './app/component-library/components-temp/TagColored/TagColored.stories.tsx': require('../app/component-library/components-temp/TagColored/TagColored.stories.tsx'),
+ './app/component-library/components-temp/TitleStandard/TitleStandard.stories.tsx': require('../app/component-library/components-temp/TitleStandard/TitleStandard.stories.tsx'),
+ './app/component-library/components-temp/TitleSubpage/TitleSubpage.stories.tsx': require('../app/component-library/components-temp/TitleSubpage/TitleSubpage.stories.tsx'),
+ './app/component-library/components/Accordions/Accordion/Accordion.stories.tsx': require('../app/component-library/components/Accordions/Accordion/Accordion.stories.tsx'),
+ './app/component-library/components/Accordions/Accordion/foundation/AccordionHeader/AccordionHeader.stories.tsx': require('../app/component-library/components/Accordions/Accordion/foundation/AccordionHeader/AccordionHeader.stories.tsx'),
+ './app/component-library/components/Avatars/Avatar/Avatar.stories.tsx': require('../app/component-library/components/Avatars/Avatar/Avatar.stories.tsx'),
+ './app/component-library/components/Avatars/Avatar/variants/AvatarAccount/AvatarAccount.stories.tsx': require('../app/component-library/components/Avatars/Avatar/variants/AvatarAccount/AvatarAccount.stories.tsx'),
+ './app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.stories.tsx': require('../app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.stories.tsx'),
+ './app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.stories.tsx': require('../app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.stories.tsx'),
+ './app/component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.stories.tsx': require('../app/component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.stories.tsx'),
+ './app/component-library/components/Avatars/Avatar/variants/AvatarToken/AvatarToken.stories.tsx': require('../app/component-library/components/Avatars/Avatar/variants/AvatarToken/AvatarToken.stories.tsx'),
+ './app/component-library/components/Avatars/AvatarGroup/AvatarGroup.stories.tsx': require('../app/component-library/components/Avatars/AvatarGroup/AvatarGroup.stories.tsx'),
+ './app/component-library/components/Badges/Badge/Badge.stories.tsx': require('../app/component-library/components/Badges/Badge/Badge.stories.tsx'),
+ './app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx': require('../app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx'),
+ './app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx': require('../app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx'),
+ './app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.stories.tsx': require('../app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.stories.tsx'),
+ './app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.stories.tsx': require('../app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.stories.tsx'),
+ './app/component-library/components/Banners/Banner/Banner.stories.tsx': require('../app/component-library/components/Banners/Banner/Banner.stories.tsx'),
+ './app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories.tsx': require('../app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories.tsx'),
+ './app/component-library/components/Banners/Banner/variants/BannerTip/BannerTip.stories.tsx': require('../app/component-library/components/Banners/Banner/variants/BannerTip/BannerTip.stories.tsx'),
+ './app/component-library/components/BottomSheets/BottomSheet/BottomSheet.stories.tsx': require('../app/component-library/components/BottomSheets/BottomSheet/BottomSheet.stories.tsx'),
+ './app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx': require('../app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx'),
+ './app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx': require('../app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx'),
+ './app/component-library/components/Buttons/Button/Button.stories.tsx': require('../app/component-library/components/Buttons/Button/Button.stories.tsx'),
+ './app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.stories.tsx': require('../app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.stories.tsx'),
+ './app/component-library/components/Buttons/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx': require('../app/component-library/components/Buttons/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx'),
+ './app/component-library/components/Buttons/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx': require('../app/component-library/components/Buttons/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx'),
+ './app/component-library/components/Buttons/ButtonIcon/ButtonIcon.stories.tsx': require('../app/component-library/components/Buttons/ButtonIcon/ButtonIcon.stories.tsx'),
+ './app/component-library/components/Cards/Card/Card.stories.tsx': require('../app/component-library/components/Cards/Card/Card.stories.tsx'),
+ './app/component-library/components/Cells/Cell/Cell.stories.tsx': require('../app/component-library/components/Cells/Cell/Cell.stories.tsx'),
+ './app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.stories.tsx': require('../app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.stories.tsx'),
+ './app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.stories.tsx': require('../app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.stories.tsx'),
+ './app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.stories.tsx': require('../app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.stories.tsx'),
+ './app/component-library/components/Checkbox/Checkbox.stories.tsx': require('../app/component-library/components/Checkbox/Checkbox.stories.tsx'),
+ './app/component-library/components/design-system.stories.tsx': require('../app/component-library/components/design-system.stories.tsx'),
+ './app/component-library/components/Form/HelpText/HelpText.stories.tsx': require('../app/component-library/components/Form/HelpText/HelpText.stories.tsx'),
+ './app/component-library/components/Form/Label/Label.stories.tsx': require('../app/component-library/components/Form/Label/Label.stories.tsx'),
+ './app/component-library/components/Form/TextField/foundation/Input/Input.stories.tsx': require('../app/component-library/components/Form/TextField/foundation/Input/Input.stories.tsx'),
+ './app/component-library/components/Form/TextField/TextField.stories.tsx': require('../app/component-library/components/Form/TextField/TextField.stories.tsx'),
+ './app/component-library/components/Form/TextFieldSearch/TextFieldSearch.stories.tsx': require('../app/component-library/components/Form/TextFieldSearch/TextFieldSearch.stories.tsx'),
+ './app/component-library/components/HeaderBase/HeaderBase.stories.tsx': require('../app/component-library/components/HeaderBase/HeaderBase.stories.tsx'),
+ './app/component-library/components/Icons/Icon/Icon.stories.tsx': require('../app/component-library/components/Icons/Icon/Icon.stories.tsx'),
+ './app/component-library/components/List/ListItem/ListItem.stories.tsx': require('../app/component-library/components/List/ListItem/ListItem.stories.tsx'),
+ './app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.stories.tsx': require('../app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.stories.tsx'),
+ './app/component-library/components/List/ListItemSelect/ListItemSelect.stories.tsx': require('../app/component-library/components/List/ListItemSelect/ListItemSelect.stories.tsx'),
+ './app/component-library/components/Modals/ModalConfirmation/ModalConfirmation.stories.tsx': require('../app/component-library/components/Modals/ModalConfirmation/ModalConfirmation.stories.tsx'),
+ './app/component-library/components/Modals/ModalMandatory/ModalMandatory.stories.tsx': require('../app/component-library/components/Modals/ModalMandatory/ModalMandatory.stories.tsx'),
+ './app/component-library/components/Navigation/TabBar/TabBar.stories.tsx': require('../app/component-library/components/Navigation/TabBar/TabBar.stories.tsx'),
+ './app/component-library/components/Navigation/TabBarItem/TabBarItem.stories.tsx': require('../app/component-library/components/Navigation/TabBarItem/TabBarItem.stories.tsx'),
+ './app/component-library/components/Overlay/Overlay.stories.tsx': require('../app/component-library/components/Overlay/Overlay.stories.tsx'),
+ './app/component-library/components/Pickers/PickerAccount/PickerAccount.stories.tsx': require('../app/component-library/components/Pickers/PickerAccount/PickerAccount.stories.tsx'),
+ './app/component-library/components/Pickers/PickerBase/PickerBase.stories.tsx': require('../app/component-library/components/Pickers/PickerBase/PickerBase.stories.tsx'),
+ './app/component-library/components/Pickers/PickerNetwork/PickerNetwork.stories.tsx': require('../app/component-library/components/Pickers/PickerNetwork/PickerNetwork.stories.tsx'),
+ './app/component-library/components/RadioButton/RadioButton.stories.tsx': require('../app/component-library/components/RadioButton/RadioButton.stories.tsx'),
+ './app/component-library/components/Select/SelectButton/SelectButton.stories.tsx': require('../app/component-library/components/Select/SelectButton/SelectButton.stories.tsx'),
+ './app/component-library/components/Select/SelectOption/SelectOption.stories.tsx': require('../app/component-library/components/Select/SelectOption/SelectOption.stories.tsx'),
+ './app/component-library/components/Select/SelectValue/SelectValue.stories.tsx': require('../app/component-library/components/Select/SelectValue/SelectValue.stories.tsx'),
+ './app/component-library/components/Sheet/SheetHeader/SheetHeader.stories.tsx': require('../app/component-library/components/Sheet/SheetHeader/SheetHeader.stories.tsx'),
+ './app/component-library/components/Skeleton/Skeleton.stories.tsx': require('../app/component-library/components/Skeleton/Skeleton.stories.tsx'),
+ './app/component-library/components/Tags/Tag/Tag.stories.tsx': require('../app/component-library/components/Tags/Tag/Tag.stories.tsx'),
+ './app/component-library/components/Tags/TagUrl/TagUrl.stories.tsx': require('../app/component-library/components/Tags/TagUrl/TagUrl.stories.tsx'),
+ './app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx': require('../app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx'),
+ './app/component-library/components/Texts/Text/Text.stories.tsx': require('../app/component-library/components/Texts/Text/Text.stories.tsx'),
+ './app/component-library/components/Texts/TextWithPrefixIcon/TextWithPrefixIcon.stories.tsx': require('../app/component-library/components/Texts/TextWithPrefixIcon/TextWithPrefixIcon.stories.tsx'),
+ './app/component-library/components/Toast/Toast.stories.tsx': require('../app/component-library/components/Toast/Toast.stories.tsx'),
+ './app/component-library/components/Toast/ToastEarn.stories.tsx': require('../app/component-library/components/Toast/ToastEarn.stories.tsx'),
+ './app/component-library/components/Toast/ToastEdgeCases.stories.tsx': require('../app/component-library/components/Toast/ToastEdgeCases.stories.tsx'),
+ './app/component-library/components/Toast/ToastMoney.stories.tsx': require('../app/component-library/components/Toast/ToastMoney.stories.tsx'),
+ './app/component-library/components/Toast/ToastPerps.stories.tsx': require('../app/component-library/components/Toast/ToastPerps.stories.tsx'),
+ './app/component-library/components/Toast/ToastPerpsWithdraw.stories.tsx': require('../app/component-library/components/Toast/ToastPerpsWithdraw.stories.tsx'),
+ './app/component-library/components/Toast/ToastPredict.stories.tsx': require('../app/component-library/components/Toast/ToastPredict.stories.tsx'),
+ './app/component-library/components/Toast/ToastQuickBuy.stories.tsx': require('../app/component-library/components/Toast/ToastQuickBuy.stories.tsx'),
+ './app/component-library/components/Toast/ToastRamp.stories.tsx': require('../app/component-library/components/Toast/ToastRamp.stories.tsx'),
+ './app/component-library/components/Toast/ToastRewards.stories.tsx': require('../app/component-library/components/Toast/ToastRewards.stories.tsx'),
+ './app/components/Base/Keypad/Keypad.stories.tsx': require('../app/components/Base/Keypad/Keypad.stories.tsx'),
+ './app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx': require('../app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx'),
+ './app/components/UI/CollectiblesEmptyState/CollectiblesEmptyState.stories.tsx': require('../app/components/UI/CollectiblesEmptyState/CollectiblesEmptyState.stories.tsx'),
+ './app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx': require('../app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx'),
+ './app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.stories.tsx': require('../app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.stories.tsx'),
+ './app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.stories.tsx': require('../app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.stories.tsx'),
+ './app/components/UI/Perps/Views/PerpsEmptyState/PerpsEmptyState.stories.tsx': require('../app/components/UI/Perps/Views/PerpsEmptyState/PerpsEmptyState.stories.tsx'),
+ './app/components/UI/Ramp/Aggregator/components/ShapesBackgroundAnimation/index.stories.tsx': require('../app/components/UI/Ramp/Aggregator/components/ShapesBackgroundAnimation/index.stories.tsx'),
+ './app/components/UI/Rewards/components/RewardPointsAnimation/RewardPointsAnimation.stories.tsx': require('../app/components/UI/Rewards/components/RewardPointsAnimation/RewardPointsAnimation.stories.tsx'),
+ './app/components/UI/SettingsButtonSection/SettingsButtonSection.stories.tsx': require('../app/components/UI/SettingsButtonSection/SettingsButtonSection.stories.tsx'),
+ './app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx': require('../app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx'),
+ './app/components/Views/confirmations/components/deposit-keyboard/deposit-keyboard.stories.tsx': require('../app/components/Views/confirmations/components/deposit-keyboard/deposit-keyboard.stories.tsx'),
+ './app/components/Views/confirmations/components/edit-amount-keyboard/edit-amount-keyboard.stories.tsx': require('../app/components/Views/confirmations/components/edit-amount-keyboard/edit-amount-keyboard.stories.tsx'),
+ './app/components/Views/ConnectHardware/HardwareWalletDiscoveryFlow/screens/errors/index.stories.tsx': require('../app/components/Views/ConnectHardware/HardwareWalletDiscoveryFlow/screens/errors/index.stories.tsx'),
+ './app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.stories.tsx': require('../app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.stories.tsx'),
+ './app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.stories.tsx': require('../app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.stories.tsx'),
+ './app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.stories.tsx': require('../app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/MultichainAccountsConnectedList.stories.tsx'),
+ './app/components/Views/MultichainAccounts/MultichainPermissionsSummary/MultichainPermissionsSummary.stories.tsx': require('../app/components/Views/MultichainAccounts/MultichainPermissionsSummary/MultichainPermissionsSummary.stories.tsx'),
+ './app/components/Views/QRAccountDisplay/QRAccountDisplay.stories.tsx': require('../app/components/Views/QRAccountDisplay/QRAccountDisplay.stories.tsx'),
+ });
configure(getStories, module, false);
diff --git a/app/component-library/components-temp/BaseNotification/BaseNotification.stories.tsx b/app/component-library/components-temp/BaseNotification/BaseNotification.stories.tsx
new file mode 100644
index 00000000000..97a4dd6c6c7
--- /dev/null
+++ b/app/component-library/components-temp/BaseNotification/BaseNotification.stories.tsx
@@ -0,0 +1,181 @@
+import React, { useCallback, useState } from 'react';
+import { ScrollView, View } from 'react-native';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import Button, { ButtonVariants } from '../../components/Buttons/Button';
+import BaseNotification from './index';
+import type {
+ BaseNotificationData,
+ BaseNotificationProps,
+ BaseNotificationStatus,
+} from './BaseNotification.types';
+
+interface NotificationTrigger {
+ label: string;
+ status: BaseNotificationStatus;
+ data?: BaseNotificationData;
+ autoDismiss?: boolean;
+ onHide?: () => void;
+}
+
+interface ActiveNotification extends NotificationTrigger {
+ key: number;
+}
+
+interface NotificationTriggerSection {
+ title: string;
+ triggers: NotificationTrigger[];
+}
+
+const STORYBOOK_PROPS: Pick<
+ BaseNotificationProps,
+ 'persistUntilDismiss' | 'isVisible'
+> = {
+ isVisible: true,
+ persistUntilDismiss: true,
+};
+
+const LOADING_TRIGGERS: NotificationTrigger[] = [
+ { label: 'Pending', status: 'pending' },
+ { label: 'Pending deposit', status: 'pending_deposit' },
+ { label: 'Pending withdrawal', status: 'pending_withdrawal' },
+ {
+ label: 'Speedup',
+ status: 'speedup',
+ data: { nonce: '12' },
+ },
+];
+
+const SUCCESS_TRIGGERS: NotificationTrigger[] = [
+ {
+ label: 'Success',
+ status: 'success',
+ data: { nonce: '42' },
+ },
+ { label: 'Success deposit', status: 'success_deposit' },
+ { label: 'Success withdrawal', status: 'success_withdrawal' },
+ {
+ label: 'Received',
+ status: 'received',
+ data: { amount: '0.5', assetType: 'ETH' },
+ },
+ { label: 'Received payment', status: 'received_payment' },
+ { label: 'ETH received', status: 'eth_received' },
+ { label: 'Import success', status: 'import_success' },
+ { label: 'Simple notification', status: 'simple_notification' },
+];
+
+const FAILURE_TRIGGERS: NotificationTrigger[] = [
+ { label: 'Error', status: 'error' },
+ { label: 'Cancelled', status: 'cancelled' },
+ {
+ label: 'Simple notification rejected',
+ status: 'simple_notification_rejected',
+ },
+];
+
+const INTERACTION_TRIGGERS: NotificationTrigger[] = [
+ {
+ label: 'Custom copy',
+ status: 'simple_notification',
+ data: {
+ title: 'Transaction submitted',
+ description: 'Your swap is being processed on Ethereum Mainnet.',
+ },
+ },
+ {
+ label: 'With close button',
+ status: 'success',
+ autoDismiss: true,
+ data: {
+ title: 'Wallet ready',
+ description: 'Your wallet was imported successfully.',
+ },
+ onHide: () => {
+ // eslint-disable-next-line no-console
+ console.log('BaseNotification dismissed');
+ },
+ },
+];
+
+const STORY_SECTIONS: NotificationTriggerSection[] = [
+ { title: 'Loading', triggers: LOADING_TRIGGERS },
+ { title: 'Success', triggers: SUCCESS_TRIGGERS },
+ { title: 'Failure', triggers: FAILURE_TRIGGERS },
+ { title: 'Interaction', triggers: INTERACTION_TRIGGERS },
+];
+
+const StoryContainer = ({ children }: { children: React.ReactNode }) => {
+ const tw = useTailwind();
+
+ return (
+
+ {children}
+
+ );
+};
+
+const NotificationTriggerStory = ({
+ sections,
+}: {
+ sections: NotificationTriggerSection[];
+}) => {
+ const tw = useTailwind();
+ const [active, setActive] = useState(null);
+
+ const showNotification = useCallback((trigger: NotificationTrigger) => {
+ setActive(null);
+ setTimeout(() => {
+ setActive({
+ key: Date.now(),
+ ...trigger,
+ });
+ }, 50);
+ }, []);
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+
+ ))}
+
+ {active && (
+ setActive(null)}
+ />
+ )}
+
+ );
+};
+
+const BaseNotificationMeta = {
+ title: 'Components Temp / BaseNotification',
+ component: BaseNotification,
+};
+
+export default BaseNotificationMeta;
+
+export const Default = {
+ render: () => ,
+};
diff --git a/app/component-library/components-temp/BaseNotification/BaseNotification.styles.ts b/app/component-library/components-temp/BaseNotification/BaseNotification.styles.ts
index 0f39f8b6961..d8b6242e777 100644
--- a/app/component-library/components-temp/BaseNotification/BaseNotification.styles.ts
+++ b/app/component-library/components-temp/BaseNotification/BaseNotification.styles.ts
@@ -1,39 +1,67 @@
-import { StyleSheet } from 'react-native';
-import { Theme } from '../../../util/theme/models';
+import { Dimensions, StyleSheet } from 'react-native';
+import { AppThemeKey, Theme } from '../../../util/theme/models';
+import {
+ TOAST_CLOSE_MARGIN_TOP,
+ TOAST_DESCRIPTION_MARGIN_TOP,
+ TOAST_PADDING_HORIZONTAL,
+ TOAST_PADDING_RIGHT_WITH_CLOSE_ICON,
+ TOAST_PADDING_VERTICAL,
+ TOAST_ROW_GAP,
+} from '../../components/Toast/Toast.constants';
+
+const marginWidth = 16;
+const notificationWidth = Dimensions.get('window').width - marginWidth * 2;
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
- const { colors } = theme;
+ const { colors, shadows } = theme;
return StyleSheet.create({
- floatingBackground: {
- backgroundColor: colors.background.section,
- marginHorizontal: 16,
- borderRadius: 8,
+ base: {
+ position: 'absolute',
+ top: 0,
+ left: marginWidth,
+ width: notificationWidth,
+ backgroundColor:
+ theme.themeAppearance === AppThemeKey.light
+ ? colors.background.default
+ : colors.background.section,
+ ...(theme.themeAppearance === AppThemeKey.light ? shadows.size.md : {}),
+ borderRadius: 16,
borderWidth: 1,
borderColor: colors.border.muted,
- },
- defaultFlashFloating: {
- padding: 16,
+ paddingTop: TOAST_PADDING_VERTICAL,
+ paddingBottom: TOAST_PADDING_VERTICAL,
+ paddingLeft: TOAST_PADDING_HORIZONTAL,
+ paddingRight: TOAST_PADDING_HORIZONTAL,
flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: TOAST_ROW_GAP,
+ },
+ baseWithCloseIconButton: {
+ paddingRight: TOAST_PADDING_RIGHT_WITH_CLOSE_ICON,
+ },
+ pressableContent: {
flex: 1,
- borderRadius: 8,
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: TOAST_ROW_GAP,
},
flashLabel: {
flex: 1,
- flexDirection: 'column',
+ justifyContent: 'flex-start',
+ },
+ flashTitle: {
+ color: colors.text.default,
},
flashText: {
- flex: 1,
- lineHeight: 18,
+ marginTop: TOAST_DESCRIPTION_MARGIN_TOP,
},
- flashTitle: {
- flex: 1,
- marginBottom: 2,
- lineHeight: 18,
+ startAccessoryContainer: {
+ alignSelf: 'flex-start',
},
- flashIcon: {
- marginRight: 15,
+ closeButton: {
+ marginTop: TOAST_CLOSE_MARGIN_TOP,
},
});
};
diff --git a/app/component-library/components-temp/BaseNotification/BaseNotification.types.ts b/app/component-library/components-temp/BaseNotification/BaseNotification.types.ts
index d242a2e41d5..83c768fed98 100644
--- a/app/component-library/components-temp/BaseNotification/BaseNotification.types.ts
+++ b/app/component-library/components-temp/BaseNotification/BaseNotification.types.ts
@@ -30,4 +30,12 @@ export interface BaseNotificationProps {
onPress?: () => void;
onHide?: () => void;
autoDismiss?: boolean;
+ /** When false the notification is not rendered. Defaults to true. */
+ isVisible?: boolean;
+ /** Called after the exit spring animation completes. */
+ onDismissComplete?: () => void;
+ /** Auto-dismiss delay in ms. Defaults to Toast visibilityDuration (2750ms). */
+ dismissDuration?: number;
+ /** When true, the notification stays visible until manually dismissed. */
+ persistUntilDismiss?: boolean;
}
diff --git a/app/component-library/components-temp/BaseNotification/index.test.tsx b/app/component-library/components-temp/BaseNotification/index.test.tsx
index 2a9539e0a86..d37f7829c27 100644
--- a/app/component-library/components-temp/BaseNotification/index.test.tsx
+++ b/app/component-library/components-temp/BaseNotification/index.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { fireEvent } from '@testing-library/react-native';
+import { act, fireEvent } from '@testing-library/react-native';
import BaseNotification, { getDescription } from './';
import renderWithProvider from '../../../util/test/renderWithProvider';
import { strings } from '../../../../locales/i18n';
@@ -85,18 +85,30 @@ describe('BaseNotification', () => {
expect(onPress).toHaveBeenCalledTimes(1);
});
- it('renders the close affordance when autoDismiss is true', () => {
+ it('invokes onHide when the close affordance is pressed', async () => {
+ jest.useFakeTimers();
const onHide = jest.fn();
const { getByTestId } = renderWithProvider(
,
);
- fireEvent.press(getByTestId('base-notification-close'));
+
+ fireEvent(getByTestId('base-notification-container'), 'layout', {
+ nativeEvent: { layout: { height: 100, width: 300, x: 0, y: 0 } },
+ });
+
+ await act(async () => {
+ fireEvent.press(getByTestId('base-notification-close'));
+ jest.runAllTimers();
+ });
+
expect(onHide).toHaveBeenCalledTimes(1);
+ jest.useRealTimers();
});
describe('EIP-7702 transactions (without nonce)', () => {
diff --git a/app/component-library/components-temp/BaseNotification/index.tsx b/app/component-library/components-temp/BaseNotification/index.tsx
index 0f6b108b5b8..b91b1e09024 100644
--- a/app/component-library/components-temp/BaseNotification/index.tsx
+++ b/app/component-library/components-temp/BaseNotification/index.tsx
@@ -1,5 +1,21 @@
-import React from 'react';
-import { TouchableOpacity, View } from 'react-native';
+import React, { useCallback, useMemo, useRef } from 'react';
+import {
+ Dimensions,
+ LayoutChangeEvent,
+ StyleProp,
+ TouchableOpacity,
+ View,
+ ViewStyle,
+} from 'react-native';
+import Animated, {
+ cancelAnimation,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withDelay,
+ withSpring,
+} from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {
Text,
TextVariant,
@@ -15,10 +31,14 @@ import {
ButtonIconSize,
} from '@metamask/design-system-react-native';
-import { baseStyles } from '../../../styles/common';
import { strings } from '../../../../locales/i18n';
import { useStyles } from '../../hooks';
import { ToastSelectorsIDs } from '../../components/Toast/ToastModal.testIds';
+import {
+ TOAST_SPRING_CONFIG,
+ TOAST_TOP_PADDING,
+ visibilityDuration,
+} from '../../components/Toast/Toast.constants';
import styleSheet from './BaseNotification.styles';
import {
@@ -27,6 +47,11 @@ import {
BaseNotificationStatus,
} from './BaseNotification.types';
+const screenHeight = Dimensions.get('window').height;
+
+const getHiddenTranslateY = (height: number, offset: number) =>
+ -(height + offset);
+
export const getIcon = (status: BaseNotificationStatus | undefined) => {
switch (status) {
case 'pending':
@@ -73,7 +98,7 @@ const getTitle = (
case 'pending_withdrawal':
return strings('notifications.pending_withdrawal_title');
case 'success': {
- const parsed = nonce != null ? parseInt(String(nonce)) : NaN;
+ const parsed = nonce != null ? parseInt(String(nonce), 10) : NaN;
if (!Number.isNaN(parsed)) {
return strings('notifications.success_title', { nonce: parsed });
}
@@ -88,7 +113,7 @@ const getTitle = (
case 'received':
return strings('notifications.received_title', { amount, assetType });
case 'speedup': {
- const parsed = nonce != null ? parseInt(String(nonce)) : NaN;
+ const parsed = nonce != null ? parseInt(String(nonce), 10) : NaN;
if (!Number.isNaN(parsed)) {
return strings('notifications.speedup_title', { nonce: parsed });
}
@@ -123,49 +148,165 @@ const BaseNotification: React.FC = ({
onPress,
onHide,
autoDismiss = false,
+ isVisible = true,
+ onDismissComplete,
+ dismissDuration,
+ persistUntilDismiss = false,
}) => {
const { styles } = useStyles(styleSheet, {});
+ const { top: topInset } = useSafeAreaInsets();
const safeData: BaseNotificationData = data ?? {};
const { description = null, title = null } = safeData;
+ const notificationHeight = useSharedValue(screenHeight);
+ const translateYProgress = useSharedValue(-screenHeight);
+ const hasEnteredRef = useRef(false);
+ const dismissDurationMs = dismissDuration ?? visibilityDuration;
+
+ const topOffset = 0;
+ const hasCloseIconButton = autoDismiss;
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ translateY: translateYProgress.value + topOffset }],
+ }));
+ const baseStyle: StyleProp = useMemo(
+ () => [
+ styles.base,
+ hasCloseIconButton && styles.baseWithCloseIconButton,
+ animatedStyle,
+ ],
+ [
+ styles.base,
+ styles.baseWithCloseIconButton,
+ animatedStyle,
+ hasCloseIconButton,
+ ],
+ );
+
+ const runExitAnimation = useCallback(
+ (onComplete?: () => void) => {
+ const hiddenTranslateY = getHiddenTranslateY(
+ notificationHeight.value,
+ topOffset,
+ );
+
+ translateYProgress.value = withSpring(
+ hiddenTranslateY,
+ TOAST_SPRING_CONFIG,
+ () => {
+ if (onComplete) {
+ runOnJS(onComplete)();
+ }
+ },
+ );
+ },
+ [notificationHeight, topOffset, translateYProgress],
+ );
+
+ const handleDismissComplete = useCallback(() => {
+ onDismissComplete?.();
+ }, [onDismissComplete]);
+
+ const handleManualDismiss = useCallback(() => {
+ cancelAnimation(translateYProgress);
+ runExitAnimation(() => {
+ onHide?.();
+ handleDismissComplete();
+ });
+ }, [handleDismissComplete, onHide, runExitAnimation, translateYProgress]);
+
+ const onAnimatedViewLayout = useCallback(
+ (event: LayoutChangeEvent) => {
+ if (!isVisible || hasEnteredRef.current) {
+ return;
+ }
+
+ hasEnteredRef.current = true;
+ const { height } = event.nativeEvent.layout;
+ const hiddenTranslateY = getHiddenTranslateY(height, topOffset);
+ const visibleTranslateY = topInset + TOAST_TOP_PADDING;
+
+ notificationHeight.value = height;
+ translateYProgress.value = hiddenTranslateY;
+
+ if (persistUntilDismiss) {
+ translateYProgress.value = withSpring(
+ visibleTranslateY,
+ TOAST_SPRING_CONFIG,
+ );
+ return;
+ }
+
+ translateYProgress.value = withSpring(
+ visibleTranslateY,
+ TOAST_SPRING_CONFIG,
+ () => {
+ translateYProgress.value = withDelay(
+ dismissDurationMs,
+ withSpring(hiddenTranslateY, TOAST_SPRING_CONFIG, () => {
+ runOnJS(handleDismissComplete)();
+ }),
+ );
+ },
+ );
+ },
+ [
+ dismissDurationMs,
+ handleDismissComplete,
+ isVisible,
+ notificationHeight,
+ persistUntilDismiss,
+ topInset,
+ topOffset,
+ translateYProgress,
+ ],
+ );
+
+ if (!isVisible) {
+ return null;
+ }
+
return (
-
-
-
- {getIcon(status)}
-
-
- {!title ? getTitle(status, safeData) : title}
-
-
- {!description ? getDescription(status, safeData) : description}
-
-
- {autoDismiss && (
-
- )}
-
-
-
+
+
+ {getIcon(status)}
+
+
+ {!title ? getTitle(status, safeData) : title}
+
+
+ {!description ? getDescription(status, safeData) : description}
+
+
+
+ {autoDismiss && (
+
+ )}
+
);
};
diff --git a/app/component-library/components/Toast/Toast.constants.ts b/app/component-library/components/Toast/Toast.constants.ts
index c87097e7921..56e88ec3fa4 100644
--- a/app/component-library/components/Toast/Toast.constants.ts
+++ b/app/component-library/components/Toast/Toast.constants.ts
@@ -1,10 +1,54 @@
-/* eslint-disable import-x/prefer-default-export */
+import type { WithSpringConfig } from 'react-native-reanimated';
+import { ImageSourcePropType } from 'react-native';
-// External dependencies.
import { AvatarAccountType } from '../Avatars/Avatar/variants/AvatarAccount';
+export const visibilityDuration = 2750;
+
+export const TOAST_TOP_PADDING = 8;
+
+/**
+ * BannerBase-aligned spacing (design-system Box tokens; 1 unit = 4px).
+ * @see https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
+ */
+export const TOAST_PADDING_VERTICAL = 12;
+export const TOAST_PADDING_BOTTOM_WITH_ACTION = 16;
+export const TOAST_PADDING_HORIZONTAL = 16;
+export const TOAST_PADDING_RIGHT_WITH_CLOSE_ICON = 8;
+export const TOAST_ROW_GAP = 16;
+/** Circular background for icon toasts when `backgroundColor` is set (24px icon inside). */
+export const TOAST_ICON_BACKGROUND_SIZE = 32;
+export const TOAST_ACTION_MARGIN_TOP = 8;
+export const TOAST_DESCRIPTION_MARGIN_TOP = 2;
+export const TOAST_CLOSE_MARGIN_TOP = -4;
+
export const TEST_ACCOUNT_ADDRESS =
- '0x10e08af911f2e489480fb2855b24771745d0198b50f5c55891369844a8c57092';
+ '0x2990079bcdEe240329a520d2444386FC119da21a';
+
+export const TEST_AVATAR_TYPE = AvatarAccountType.JazzIcon;
+
+export const TEST_NETWORK_NAME = 'Ethereum Mainnet';
+
export const TEST_NETWORK_IMAGE_URL =
'https://assets.coingecko.com/coins/images/279/small/ethereum.png?1595348880';
-export const TEST_AVATAR_TYPE = AvatarAccountType.JazzIcon;
+
+export const TEST_NETWORK_IMAGE_SOURCE: ImageSourcePropType = {
+ uri: TEST_NETWORK_IMAGE_URL,
+};
+
+export const TEST_APP_ICON_SOURCE: ImageSourcePropType = {
+ uri: 'https://app.uniswap.org/favicon.ico',
+};
+
+/**
+ * Spring tuned to approximate iOS system banner motion.
+ *
+ * UIKit reference: `animate(withDuration: 0.5, usingSpringWithDamping: 0.7,
+ * initialSpringVelocity: 1)`
+ *
+ * SwiftUI reference: `.snappy` / `.smooth` with minimal bounce.
+ */
+export const TOAST_SPRING_CONFIG: WithSpringConfig = {
+ dampingRatio: 0.85,
+ duration: 500,
+};
diff --git a/app/component-library/components/Toast/Toast.stories.tsx b/app/component-library/components/Toast/Toast.stories.tsx
index 60ee9a04a26..6010b46d47a 100644
--- a/app/component-library/components/Toast/Toast.stories.tsx
+++ b/app/component-library/components/Toast/Toast.stories.tsx
@@ -1,120 +1,354 @@
-// Third party dependencies.
-import React, { useContext } from 'react';
-import { Alert, View } from 'react-native';
-import { SafeAreaProvider } from 'react-native-safe-area-context';
-import type { Meta } from '@storybook/react-native';
-
-// External dependencies.
-import Button, { ButtonVariants } from '../Buttons/Button';
+import React, { useCallback, useContext, useMemo } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import {
+ IconColor as ReactNativeDsIconColor,
+ IconSize as ReactNativeDsIconSize,
+ Spinner,
+ Text,
+ TextVariant,
+} from '@metamask/design-system-react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
-// Internal dependencies.
-import { default as ToastComponent } from './Toast';
+import { useAppThemeFromContext } from '../../../util/theme';
+import type { Theme } from '../../../util/theme/models';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import { IconName } from '../Icons/Icon';
+import Toast from './Toast';
import { ToastContext, ToastContextWrapper } from './Toast.context';
-import { ToastOptions, ToastVariants } from './Toast.types';
import {
TEST_ACCOUNT_ADDRESS,
+ TEST_APP_ICON_SOURCE,
TEST_AVATAR_TYPE,
- TEST_NETWORK_IMAGE_URL,
+ TEST_NETWORK_IMAGE_SOURCE,
+ TEST_NETWORK_NAME,
} from './Toast.constants';
+import { ButtonIconVariant, ToastOptions, ToastVariants } from './Toast.types';
+import {
+ presentStoryToast,
+ STORYBOOK_TOAST_OPTIONS,
+ StoryContainer,
+ StoryToastHost,
+} from './ToastStory.shared';
-interface ToastStoryArgs {
- variant: ToastVariants;
+interface ToastTrigger {
+ label: string;
+ getOptions: (theme: Theme) => ToastOptions;
}
-export default {
- title: 'Component Library / Toast',
- component: ToastComponent,
- decorators: [
- (StoryComponent) => (
-
-
-
-
-
- ),
- ],
- argTypes: {
- variant: {
- options: ToastVariants,
- control: {
- type: 'select',
+interface ToastTriggerSection {
+ title: string;
+ triggers: ToastTrigger[];
+}
+
+const getPlainTriggers = (): ToastTrigger[] => [
+ {
+ label: 'Plain',
+ getOptions: () => ({
+ variant: ToastVariants.Plain,
+ hasNoTimeout: true,
+ labelOptions: [{ label: 'This is a plain toast message.' }],
+ }),
+ },
+ {
+ label: 'Plain with description',
+ getOptions: () => ({
+ variant: ToastVariants.Plain,
+ hasNoTimeout: true,
+ labelOptions: [{ label: 'Transaction submitted', isBold: true }],
+ descriptionOptions: {
+ description: 'Your swap is being processed on Ethereum Mainnet.',
},
- defaultValue: ToastVariants.Plain,
- },
+ }),
+ },
+ {
+ label: 'Plain with bold labels',
+ getOptions: () => ({
+ variant: ToastVariants.Plain,
+ hasNoTimeout: true,
+ labelOptions: [
+ { label: 'Switching to' },
+ { label: ' Account 2', isBold: true },
+ { label: '.' },
+ ],
+ }),
},
-} as Meta;
+];
-export const Default = {
- args: {
- variant: ToastVariants.Plain,
- },
- render: function Render(args: ToastStoryArgs) {
- const { toastRef } = useContext(ToastContext);
- const tw = useTailwind();
-
- let toastOptions: ToastOptions;
-
- switch (args.variant) {
- case ToastVariants.Plain:
- toastOptions = {
- variant: ToastVariants.Plain,
- hasNoTimeout: false,
- labelOptions: [{ label: 'This is a Toast message.' }],
- };
- break;
- case ToastVariants.Account:
- toastOptions = {
- variant: ToastVariants.Account,
- hasNoTimeout: false,
- labelOptions: [
- { label: 'Switching to' },
- { label: ' Account 2.', isBold: true },
- ],
- accountAddress: TEST_ACCOUNT_ADDRESS,
- accountAvatarType: TEST_AVATAR_TYPE,
- };
- break;
- case ToastVariants.Network:
- toastOptions = {
- variant: ToastVariants.Network,
- hasNoTimeout: false,
- labelOptions: [
- { label: 'Added' },
- { label: ' Mainnet', isBold: true },
- { label: ' network.' },
- ],
- networkImageSource: { uri: TEST_NETWORK_IMAGE_URL },
- descriptionOptions: {
- description: 'This is a description text for the network toast.',
- },
- linkButtonOptions: {
- label: 'Click here!',
- onPress: () => {
- Alert.alert('Clicked toast link!');
- },
- },
- };
- break;
- default:
- toastOptions = {
- variant: ToastVariants.Plain,
- hasNoTimeout: false,
- labelOptions: [{ label: 'This is a Toast message.' }],
- };
- }
-
- return (
-
-
- );
+ ),
+ }),
+ },
+ {
+ label: 'Icon error',
+ getOptions: () => ({
+ variant: ToastVariants.Icon,
+ hasNoTimeout: true,
+ iconName: IconName.Warning,
+ iconColor: theme.colors.accent01.dark,
+ backgroundColor: theme.colors.accent01.light,
+ labelOptions: [
+ { label: 'Transaction failed', isBold: true },
+ { label: '\n', isBold: false },
+ { label: 'Please try again.', isBold: false },
+ ],
+ }),
},
+ {
+ label: 'Icon warning',
+ getOptions: () => ({
+ variant: ToastVariants.Icon,
+ hasNoTimeout: true,
+ iconName: IconName.Warning,
+ iconColor: theme.colors.warning.default,
+ backgroundColor: theme.colors.warning.muted,
+ labelOptions: [
+ { label: 'Taking longer than expected', isBold: true },
+ { label: '\n', isBold: false },
+ { label: 'Your transaction is still pending.', isBold: false },
+ ],
+ }),
+ },
+ {
+ label: 'Icon info',
+ getOptions: () => ({
+ variant: ToastVariants.Icon,
+ hasNoTimeout: true,
+ iconName: IconName.Info,
+ iconColor: theme.colors.icon.default,
+ backgroundColor: theme.colors.background.alternative,
+ labelOptions: [
+ { label: 'Network switched', isBold: true },
+ { label: '\n', isBold: false },
+ { label: 'You are now connected to Linea.', isBold: false },
+ ],
+ }),
+ },
+];
+
+const getCloseActionTriggers = (): ToastTrigger[] => [
+ {
+ label: 'With close secondary button',
+ getOptions: () => ({
+ variant: ToastVariants.Icon,
+ hasNoTimeout: true,
+ iconName: IconName.CheckBold,
+ labelOptions: [
+ { label: 'Deposit complete', isBold: true },
+ { label: '\n', isBold: false },
+ { label: '100 USDC added to your account.', isBold: false },
+ ],
+ closeButtonOptions: {
+ label: 'Track',
+ variant: ButtonVariants.Secondary,
+ onPress: () => Alert.alert('Track pressed'),
+ },
+ }),
+ },
+ {
+ label: 'With close icon button',
+ getOptions: () => ({
+ variant: ToastVariants.Plain,
+ hasNoTimeout: true,
+ labelOptions: [{ label: 'Notification with dismiss icon.' }],
+ closeButtonOptions: {
+ variant: ButtonIconVariant.Icon,
+ iconName: IconName.Close,
+ onPress: () => Alert.alert('Close pressed'),
+ },
+ }),
+ },
+ {
+ label: 'With primary close button',
+ getOptions: () => ({
+ variant: ToastVariants.Plain,
+ hasNoTimeout: true,
+ labelOptions: [{ label: 'Action required' }],
+ descriptionOptions: {
+ description: 'Confirm the transaction in your wallet.',
+ },
+ closeButtonOptions: {
+ label: 'Review',
+ variant: ButtonVariants.Primary,
+ onPress: () => Alert.alert('Review pressed'),
+ },
+ }),
+ },
+ {
+ label: 'With custom top offset',
+ getOptions: () => ({
+ variant: ToastVariants.Plain,
+ hasNoTimeout: true,
+ customTopOffset: 24,
+ labelOptions: [{ label: 'Toast with extra top offset.' }],
+ }),
+ },
+];
+
+const getStorySections = (theme: Theme): ToastTriggerSection[] => [
+ { title: 'Plain', triggers: getPlainTriggers() },
+ { title: 'Account', triggers: getAccountTriggers() },
+ { title: 'Network', triggers: getNetworkTriggers() },
+ { title: 'App', triggers: getAppTriggers() },
+ { title: 'Icon', triggers: getIconTriggers(theme) },
+ { title: 'Close / action', triggers: getCloseActionTriggers() },
+];
+
+const ToastTriggerStoryContent = () => {
+ const tw = useTailwind();
+ const theme = useAppThemeFromContext();
+ const { toastRef } = useContext(ToastContext);
+ const sections = useMemo(() => getStorySections(theme), [theme]);
+
+ const showToast = useCallback(
+ (trigger: ToastTrigger) => {
+ presentStoryToast(toastRef, {
+ ...STORYBOOK_TOAST_OPTIONS,
+ ...trigger.getOptions(theme),
+ });
+ },
+ [theme, toastRef],
+ );
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+
+ ))}
+
+
+
+ );
+};
+
+const ToastTriggerStory = () => (
+
+
+
+);
+
+const ToastMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default ToastMeta;
+
+export const Default = {
+ render: () => ,
};
diff --git a/app/component-library/components/Toast/Toast.styles.ts b/app/component-library/components/Toast/Toast.styles.ts
index 0ad737cb36a..8df25c9be0b 100644
--- a/app/component-library/components/Toast/Toast.styles.ts
+++ b/app/component-library/components/Toast/Toast.styles.ts
@@ -1,9 +1,20 @@
// Third party dependencies.
import { StyleSheet, Dimensions } from 'react-native';
-import { Theme } from '../../../util/theme/models';
+import { AppThemeKey, Theme } from '../../../util/theme/models';
+
+import {
+ TOAST_ACTION_MARGIN_TOP,
+ TOAST_CLOSE_MARGIN_TOP,
+ TOAST_DESCRIPTION_MARGIN_TOP,
+ TOAST_PADDING_BOTTOM_WITH_ACTION,
+ TOAST_PADDING_HORIZONTAL,
+ TOAST_PADDING_RIGHT_WITH_CLOSE_ICON,
+ TOAST_PADDING_VERTICAL,
+ TOAST_ICON_BACKGROUND_SIZE,
+ TOAST_ROW_GAP,
+} from './Toast.constants';
const marginWidth = 16;
-const padding = 12;
const toastWidth = Dimensions.get('window').width - marginWidth * 2;
/**
@@ -13,37 +24,60 @@ const toastWidth = Dimensions.get('window').width - marginWidth * 2;
*/
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
- const { colors } = theme;
+ const { colors, shadows } = theme;
return StyleSheet.create({
base: {
position: 'absolute',
width: toastWidth,
left: marginWidth,
- bottom: 0,
- backgroundColor: colors.background.section,
+ top: 0,
+ backgroundColor:
+ theme.themeAppearance === AppThemeKey.light
+ ? colors.background.default
+ : colors.background.section,
+ ...(theme.themeAppearance === AppThemeKey.light ? shadows.size.md : {}),
borderWidth: 1,
borderColor: colors.border.muted,
- borderRadius: 12,
- padding,
+ borderRadius: 16,
+ paddingTop: TOAST_PADDING_VERTICAL,
+ paddingBottom: TOAST_PADDING_VERTICAL,
+ paddingLeft: TOAST_PADDING_HORIZONTAL,
+ paddingRight: TOAST_PADDING_HORIZONTAL,
flexDirection: 'row',
alignItems: 'flex-start',
- },
- avatar: {
- marginTop: -4,
- marginRight: 16,
+ gap: TOAST_ROW_GAP,
},
labelsContainer: {
flex: 1,
justifyContent: 'flex-start',
},
+ startAccessoryContainer: {
+ alignSelf: 'flex-start',
+ },
+ iconBackground: {
+ width: TOAST_ICON_BACKGROUND_SIZE,
+ height: TOAST_ICON_BACKGROUND_SIZE,
+ borderRadius: TOAST_ICON_BACKGROUND_SIZE / 2,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ baseWithActionButton: {
+ paddingBottom: TOAST_PADDING_BOTTOM_WITH_ACTION,
+ },
+ baseWithCloseIconButton: {
+ paddingRight: TOAST_PADDING_RIGHT_WITH_CLOSE_ICON,
+ },
label: {
color: colors.text.default,
},
description: {
- marginTop: 4,
+ marginTop: TOAST_DESCRIPTION_MARGIN_TOP,
},
actionButton: {
- marginTop: 8,
+ marginTop: TOAST_ACTION_MARGIN_TOP,
+ },
+ closeButton: {
+ marginTop: TOAST_CLOSE_MARGIN_TOP,
},
});
};
diff --git a/app/component-library/components/Toast/Toast.test.tsx b/app/component-library/components/Toast/Toast.test.tsx
index cbb81a03419..d454f9a4cec 100644
--- a/app/component-library/components/Toast/Toast.test.tsx
+++ b/app/component-library/components/Toast/Toast.test.tsx
@@ -106,6 +106,31 @@ describe('Toast', () => {
expect(screen.getByText('Test description')).toBeOnTheScreen();
});
+ it('hides toast with customTopOffset when closeToast is called', async () => {
+ const toastOptions: ToastOptions = {
+ variant: ToastVariants.Plain,
+ labelOptions: [{ label: 'Offset toast' }],
+ hasNoTimeout: true,
+ customTopOffset: 24,
+ };
+
+ render();
+
+ await act(async () => {
+ toastRef.current?.showToast(toastOptions);
+ jest.runAllTimers();
+ });
+
+ expect(screen.getByText('Offset toast')).toBeOnTheScreen();
+
+ await act(async () => {
+ toastRef.current?.closeToast();
+ jest.runAllTimers();
+ });
+
+ expect(screen.queryByText('Offset toast')).toBeNull();
+ });
+
it('hides toast when closeToast is called', async () => {
const toastOptions: ToastOptions = {
variant: ToastVariants.Plain,
diff --git a/app/component-library/components/Toast/Toast.tsx b/app/component-library/components/Toast/Toast.tsx
index 05181ab63e0..2923b8c6dfd 100644
--- a/app/component-library/components/Toast/Toast.tsx
+++ b/app/component-library/components/Toast/Toast.tsx
@@ -21,14 +21,20 @@ import Animated, {
useAnimatedStyle,
useSharedValue,
withDelay,
- withTiming,
+ withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// External dependencies.
import Avatar, { AvatarSize, AvatarVariant } from '../Avatars/Avatar';
+import Icon, { IconSize } from '../Icons/Icon';
import Text, { TextColor, TextVariant } from '../Texts/Text';
-import Button, { ButtonVariants } from '../Buttons/Button';
+import {
+ Button,
+ ButtonSize,
+ ButtonVariant,
+ IconName as DsIconName,
+} from '@metamask/design-system-react-native';
// Internal dependencies.
import {
@@ -43,15 +49,31 @@ import {
} from './Toast.types';
import styleSheet from './Toast.styles';
import { ToastSelectorsIDs } from './ToastModal.testIds';
-import { TAB_BAR_HEIGHT } from '../Navigation/TabBar/TabBar.constants';
+import {
+ TOAST_SPRING_CONFIG,
+ TOAST_TOP_PADDING,
+ visibilityDuration,
+} from './Toast.constants';
import { useStyles } from '../../hooks';
+import { ButtonProps, ButtonVariants } from '../Buttons/Button/Button.types';
import ButtonIcon from '../Buttons/ButtonIcon';
+import { ButtonIconSizes } from '../Buttons/ButtonIcon/ButtonIcon.types';
-const visibilityDuration = 2750;
-const animationDuration = 250;
-const bottomPadding = 36;
const screenHeight = Dimensions.get('window').height;
+const getHiddenTranslateY = (height: number, offset: number) =>
+ -(height + offset);
+
+const mapLegacyButtonVariant = (variant?: ButtonVariants): ButtonVariant => {
+ if (variant === ButtonVariants.Secondary) {
+ return ButtonVariant.Secondary;
+ }
+ if (variant === ButtonVariants.Link) {
+ return ButtonVariant.Secondary;
+ }
+ return ButtonVariant.Primary;
+};
+
/**
* @deprecated Please update your code to use `Toast` from `@metamask/design-system-react-native`.
* The API may have changed — compare props before migrating.
@@ -63,18 +85,32 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
const [toastOptions, setToastOptions] = useState(
undefined,
);
- const { bottom: bottomNotchSpacing } = useSafeAreaInsets();
- const translateYProgress = useSharedValue(screenHeight);
+ const { top: topInset } = useSafeAreaInsets();
+ const toastHeight = useSharedValue(screenHeight);
+ const translateYProgress = useSharedValue(-screenHeight);
const pendingTimeoutRef = useRef | null>(null);
- const customOffset = toastOptions?.customBottomOffset ?? 0;
+ const topOffset =
+ toastOptions?.customTopOffset ?? toastOptions?.customBottomOffset ?? 0;
const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- { translateY: translateYProgress.value - TAB_BAR_HEIGHT - customOffset },
- ],
+ transform: [{ translateY: translateYProgress.value + topOffset }],
}));
+ const hasCloseIconButton =
+ toastOptions?.closeButtonOptions?.variant === ButtonIconVariant.Icon;
const baseStyle: StyleProp = useMemo(
- () => [styles.base, animatedStyle],
- [styles.base, animatedStyle],
+ () => [
+ styles.base,
+ toastOptions?.linkButtonOptions && styles.baseWithActionButton,
+ hasCloseIconButton && styles.baseWithCloseIconButton,
+ animatedStyle,
+ ],
+ [
+ styles.base,
+ styles.baseWithActionButton,
+ styles.baseWithCloseIconButton,
+ animatedStyle,
+ toastOptions?.linkButtonOptions,
+ hasCloseIconButton,
+ ],
);
const resetState = () => setToastOptions(undefined);
@@ -101,9 +137,9 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
};
const closeToast = () => {
- translateYProgress.value = withTiming(
- screenHeight,
- { duration: animationDuration },
+ translateYProgress.value = withSpring(
+ getHiddenTranslateY(toastHeight.value, topOffset),
+ TOAST_SPRING_CONFIG,
() => {
runOnJS(resetState)();
},
@@ -118,24 +154,27 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
const onAnimatedViewLayout = (e: LayoutChangeEvent) => {
if (toastOptions) {
const { height } = e.nativeEvent.layout;
- const translateYToValue = -(bottomPadding + bottomNotchSpacing);
+ const hiddenTranslateY = getHiddenTranslateY(height, topOffset);
+ const visibleTranslateY = topInset + TOAST_TOP_PADDING;
- translateYProgress.value = height;
+ toastHeight.value = height;
+ translateYProgress.value = hiddenTranslateY;
if (toastOptions.hasNoTimeout) {
- translateYProgress.value = withTiming(translateYToValue, {
- duration: animationDuration,
- });
+ translateYProgress.value = withSpring(
+ visibleTranslateY,
+ TOAST_SPRING_CONFIG,
+ );
} else {
- translateYProgress.value = withTiming(
- translateYToValue,
- { duration: animationDuration },
+ translateYProgress.value = withSpring(
+ visibleTranslateY,
+ TOAST_SPRING_CONFIG,
() => {
translateYProgress.value = withDelay(
visibilityDuration,
- withTiming(
- height,
- { duration: animationDuration },
+ withSpring(
+ hiddenTranslateY,
+ TOAST_SPRING_CONFIG,
runOnJS(resetState),
),
);
@@ -145,13 +184,16 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
}
};
- const renderLabel = (labelOptions: ToastLabelOptions) => (
+ const renderInlineLabelSegments = (segments: ToastLabelOptions) => (
- {labelOptions.map(({ label, isBold }, index) => (
+ {segments.map(({ label, isBold }, index) => (
{label}
@@ -159,6 +201,46 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
);
+ const renderLabel = (labelOptions: ToastLabelOptions) => {
+ const descriptionSplitIndex = labelOptions.findIndex(
+ (option, index) => index > 0 && option.label === '\n',
+ );
+
+ if (descriptionSplitIndex === -1) {
+ return renderInlineLabelSegments(labelOptions);
+ }
+
+ const titleOptions = labelOptions.slice(0, descriptionSplitIndex);
+ const descriptionLabelOptions = labelOptions.slice(
+ descriptionSplitIndex + 1,
+ );
+
+ return (
+ <>
+ {titleOptions.length > 0
+ ? renderInlineLabelSegments(titleOptions)
+ : null}
+ {descriptionLabelOptions.length > 0 ? (
+
+ {descriptionLabelOptions.map(({ label }, index) => (
+
+ {label}
+
+ ))}
+
+ ) : null}
+ >
+ );
+ };
+
const renderDescription = (descriptionOptions?: ToastDescriptionOptions) =>
descriptionOptions && (
) => {
const renderActionButton = (linkButtonOptions?: ToastLinkButtonOptions) =>
linkButtonOptions && (
+ >
+ {linkButtonOptions.label}
+
);
const renderCloseButton = (closeButtonOptions?: ToastCloseButtonOptions) => {
@@ -187,17 +270,23 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
closeButtonOptions?.onPress?.()}
iconName={closeButtonOptions?.iconName}
+ size={ButtonIconSizes.Md}
+ style={styles.closeButton}
/>
);
}
+ const legacyCloseButton = closeButtonOptions as ButtonProps;
+
return (
);
};
@@ -216,7 +305,6 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
// should receive avatar type as props
type={accountAvatarType}
size={AvatarSize.Md}
- style={styles.avatar}
/>
);
}
@@ -227,8 +315,7 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
variant={AvatarVariant.Network}
name={networkName}
imageSource={networkImageSource}
- size={AvatarSize.Md}
- style={styles.avatar}
+ size={AvatarSize.Sm}
/>
);
}
@@ -239,21 +326,26 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
variant={AvatarVariant.Favicon}
imageSource={appIconSource}
size={AvatarSize.Md}
- style={styles.avatar}
/>
);
}
case ToastVariants.Icon: {
const { iconName, iconColor, backgroundColor } = toastOptions;
- return (
-
+ const icon = (
+
);
+ const hasIconBackground =
+ backgroundColor != null && backgroundColor !== 'transparent';
+
+ if (hasIconBackground) {
+ return (
+
+ {icon}
+
+ );
+ }
+
+ return icon;
}
}
};
@@ -269,10 +361,15 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => {
const isStartAccessoryValid =
startAccessory != null && React.isValidElement(startAccessory);
+ const leadingAccessory = isStartAccessoryValid
+ ? startAccessory
+ : renderAvatar();
return (
<>
- {isStartAccessoryValid ? startAccessory : renderAvatar()}
+ {leadingAccessory ? (
+ {leadingAccessory}
+ ) : null}
EarnToastOptions;
+}
+
+interface EarnToastTriggerSection {
+ title: string;
+ triggers: EarnToastTrigger[];
+}
+
+const buildEarnToastSections = (
+ options: EarnToastOptionsConfig,
+): EarnToastTriggerSection[] => [
+ {
+ title: 'mUSD conversion',
+ triggers: [
+ {
+ label: 'Conversion in progress',
+ getConfig: () =>
+ options.mUsdConversion.inProgress({
+ tokenSymbol: MOCK_TOKEN_SYMBOL,
+ }),
+ },
+ {
+ label: 'Conversion success',
+ getConfig: () => options.mUsdConversion.success,
+ },
+ {
+ label: 'Conversion failed',
+ getConfig: () => options.mUsdConversion.failed,
+ },
+ ],
+ },
+ {
+ title: 'Bonus claim',
+ triggers: [
+ {
+ label: 'Claim in progress',
+ getConfig: () => options.bonusClaim.inProgress,
+ },
+ {
+ label: 'Claim success',
+ getConfig: () => options.bonusClaim.success,
+ },
+ {
+ label: 'Claim failed',
+ getConfig: () => options.bonusClaim.failed,
+ },
+ ],
+ },
+ {
+ title: 'TRON withdrawal',
+ triggers: [
+ {
+ label: 'Withdrawal failed',
+ getConfig: () => options.tronWithdrawal.failed(MOCK_WITHDRAWAL_ERRORS),
+ },
+ {
+ label: 'Withdrawal failed (no details)',
+ getConfig: () => options.tronWithdrawal.failed([]),
+ },
+ ],
+ },
+];
+
+const EarnToastsStoryContent = () => {
+ const tw = useTailwind();
+ const { toastRef } = useContext(ToastContext);
+ const { showToast, EarnToastOptions } = useEarnToasts();
+ const sections = useMemo(
+ () => buildEarnToastSections(EarnToastOptions),
+ [EarnToastOptions],
+ );
+
+ const triggerEarnToast = useCallback(
+ (trigger: EarnToastTrigger) => {
+ showToast({
+ ...trigger.getConfig(EarnToastOptions),
+ hasNoTimeout: true,
+ });
+ },
+ [EarnToastOptions, showToast],
+ );
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+
+ ))}
+
+
+
+ );
+};
+
+const EarnToastsStory = () => (
+
+
+
+);
+
+const EarnToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default EarnToastsMeta;
+
+export const EarnToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastEdgeCases.stories.tsx b/app/component-library/components/Toast/ToastEdgeCases.stories.tsx
new file mode 100644
index 00000000000..b78ec63cbc3
--- /dev/null
+++ b/app/component-library/components/Toast/ToastEdgeCases.stories.tsx
@@ -0,0 +1,167 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import {
+ Text,
+ TextVariant,
+ Button,
+ ButtonSize,
+ ButtonVariant,
+} from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import { buildShareCopiedToastOptions } from '../../../components/UI/Predict/hooks/usePredictShare.utils';
+import { strings } from '../../../../locales/i18n';
+import { useAppThemeFromContext } from '../../../util/theme';
+import { getNetworkImageSource } from '../../../util/networks';
+import { IconName } from '../Icons/Icon';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import {
+ ButtonIconVariant,
+ ToastVariants,
+ type ToastOptions,
+} from './Toast.types';
+import { TEST_NETWORK_IMAGE_SOURCE } from './Toast.constants';
+import {
+ presentStoryToast,
+ StoryContainer,
+ StoryToastHost,
+} from './ToastStory.shared';
+
+interface EdgeCaseToastTrigger {
+ label: string;
+ getOptions: (params: {
+ successColor: string;
+ tokenImageUri?: string;
+ }) => ToastOptions;
+}
+
+const EDGE_CASE_TOAST_SECTIONS: {
+ title: string;
+ triggers: EdgeCaseToastTrigger[];
+}[] = [
+ {
+ title: 'Gas fee token (confirmations)',
+ triggers: [
+ {
+ label: 'Network token image',
+ getOptions: ({ tokenImageUri }) => ({
+ variant: ToastVariants.Network,
+ hasNoTimeout: false,
+ customBottomOffset: 24,
+ labelOptions: [
+ {
+ label: `${strings('gas_fee_token_toast.message')}USDC.`,
+ isBold: true,
+ },
+ ],
+ networkImageSource: tokenImageUri
+ ? { uri: tokenImageUri }
+ : TEST_NETWORK_IMAGE_SOURCE,
+ closeButtonOptions: {
+ variant: ButtonIconVariant.Icon,
+ iconName: IconName.Close,
+ onPress: () => Alert.alert('Storybook', 'Close pressed'),
+ },
+ }),
+ },
+ {
+ label: 'Chain fallback image',
+ getOptions: () => ({
+ variant: ToastVariants.Network,
+ hasNoTimeout: false,
+ customBottomOffset: 24,
+ labelOptions: [
+ {
+ label: `${strings('gas_fee_token_toast.message')}ETH.`,
+ isBold: true,
+ },
+ ],
+ networkImageSource: getNetworkImageSource({ chainId: '0x1' }),
+ closeButtonOptions: {
+ variant: ButtonIconVariant.Icon,
+ iconName: IconName.Close,
+ onPress: () => Alert.alert('Storybook', 'Close pressed'),
+ },
+ }),
+ },
+ ],
+ },
+ {
+ title: 'Predict share copied',
+ triggers: [
+ {
+ label: 'Copied to clipboard',
+ getOptions: ({ successColor }) =>
+ buildShareCopiedToastOptions({
+ label: strings('predict.toasts.copied_to_clipboard'),
+ successColor,
+ }),
+ },
+ ],
+ },
+];
+
+const EdgeCaseToastsStoryContent = () => {
+ const tw = useTailwind();
+ const theme = useAppThemeFromContext();
+ const { toastRef } = useContext(ToastContext);
+ const sections = useMemo(() => EDGE_CASE_TOAST_SECTIONS, []);
+
+ const triggerEdgeCaseToast = useCallback(
+ (trigger: EdgeCaseToastTrigger) => {
+ presentStoryToast(
+ toastRef,
+ trigger.getOptions({
+ successColor: theme.colors.success.default,
+ tokenImageUri:
+ 'https://assets.coingecko.com/coins/images/6319/small/usdc.png',
+ }),
+ );
+ },
+ [theme.colors.success.default, toastRef],
+ );
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+};
+
+const EdgeCaseToastsStory = () => (
+
+
+
+);
+
+const EdgeCaseToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default EdgeCaseToastsMeta;
+
+export const EdgeCases = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastMoney.stories.tsx b/app/component-library/components/Toast/ToastMoney.stories.tsx
new file mode 100644
index 00000000000..524d7dac3e9
--- /dev/null
+++ b/app/component-library/components/Toast/ToastMoney.stories.tsx
@@ -0,0 +1,218 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import { ScrollView, View } from 'react-native';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import useMoneyToasts, {
+ type MoneyToastOptions,
+ type MoneyToastOptionsConfig,
+} from '../../../components/UI/Money/hooks/useMoneyToasts';
+import { strings } from '../../../../locales/i18n';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import { StoryContainer, StoryToastHost } from './ToastStory.shared';
+
+const MOCK_FIAT_AMOUNT = '$50.00';
+const MOCK_WITHDRAW_DESTINATION = strings(
+ 'money.toasts.withdraw_fallback_destination',
+);
+
+interface MoneyToastTrigger {
+ label: string;
+ getConfig: (options: MoneyToastOptionsConfig) => MoneyToastOptions;
+}
+
+interface MoneyToastTriggerSection {
+ title: string;
+ triggers: MoneyToastTrigger[];
+}
+
+const buildMoneyToastSections = (
+ options: MoneyToastOptionsConfig,
+): MoneyToastTriggerSection[] => [
+ {
+ title: 'Deposit — convert',
+ triggers: [
+ {
+ label: 'In progress',
+ getConfig: () => options.deposit.inProgress({ intent: 'convert' }),
+ },
+ {
+ label: 'Success',
+ getConfig: () =>
+ options.deposit.success({
+ amountFiat: MOCK_FIAT_AMOUNT,
+ intent: 'convert',
+ }),
+ },
+ {
+ label: 'Success (no amount)',
+ getConfig: () => options.deposit.success({ intent: 'convert' }),
+ },
+ {
+ label: 'Failed',
+ getConfig: () => options.deposit.failed({ intent: 'convert' }),
+ },
+ ],
+ },
+ {
+ title: 'Deposit — add mUSD',
+ triggers: [
+ {
+ label: 'In progress',
+ getConfig: () => options.deposit.inProgress({ intent: 'addMusd' }),
+ },
+ {
+ label: 'Success',
+ getConfig: () =>
+ options.deposit.success({
+ amountFiat: MOCK_FIAT_AMOUNT,
+ intent: 'addMusd',
+ }),
+ },
+ {
+ label: 'Failed',
+ getConfig: () => options.deposit.failed({ intent: 'addMusd' }),
+ },
+ ],
+ },
+ {
+ title: 'Deposit — card',
+ triggers: [
+ {
+ label: 'In progress',
+ getConfig: () => options.deposit.inProgress({ intent: 'card' }),
+ },
+ {
+ label: 'Success',
+ getConfig: () =>
+ options.deposit.success({
+ amountFiat: MOCK_FIAT_AMOUNT,
+ intent: 'card',
+ }),
+ },
+ {
+ label: 'Failed',
+ getConfig: () => options.deposit.failed({ intent: 'card' }),
+ },
+ ],
+ },
+ {
+ title: 'Withdraw',
+ triggers: [
+ {
+ label: 'In progress',
+ getConfig: () => options.withdraw.inProgress(),
+ },
+ {
+ label: 'Success',
+ getConfig: () =>
+ options.withdraw.success({
+ amountFiat: MOCK_FIAT_AMOUNT,
+ destination: MOCK_WITHDRAW_DESTINATION,
+ }),
+ },
+ {
+ label: 'Success (no amount)',
+ getConfig: () =>
+ options.withdraw.success({
+ destination: MOCK_WITHDRAW_DESTINATION,
+ }),
+ },
+ {
+ label: 'Failed',
+ getConfig: () => options.withdraw.failed(),
+ },
+ ],
+ },
+ {
+ title: 'Send',
+ triggers: [
+ {
+ label: 'In progress',
+ getConfig: () => options.send.inProgress(),
+ },
+ {
+ label: 'Success — Perps',
+ getConfig: () =>
+ options.send.success({
+ amountFiat: MOCK_FIAT_AMOUNT,
+ destination: strings('money.toasts.send_destination_perps'),
+ }),
+ },
+ {
+ label: 'Success — Predict (no amount)',
+ getConfig: () =>
+ options.send.success({
+ destination: strings('money.toasts.send_destination_predict'),
+ }),
+ },
+ {
+ label: 'Failed',
+ getConfig: () => options.send.failed(),
+ },
+ ],
+ },
+];
+
+const MoneyToastsStoryContent = () => {
+ const tw = useTailwind();
+ const { toastRef } = useContext(ToastContext);
+ const { showToast, MoneyToastOptions } = useMoneyToasts();
+ const sections = useMemo(
+ () => buildMoneyToastSections(MoneyToastOptions),
+ [MoneyToastOptions],
+ );
+
+ const triggerMoneyToast = useCallback(
+ (trigger: MoneyToastTrigger) => {
+ showToast({
+ ...trigger.getConfig(MoneyToastOptions),
+ hasNoTimeout: true,
+ });
+ },
+ [MoneyToastOptions, showToast],
+ );
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+
+ ))}
+
+
+
+ );
+};
+
+const MoneyToastsStory = () => (
+
+
+
+);
+
+const MoneyToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default MoneyToastsMeta;
+
+export const MoneyToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastPerps.stories.tsx b/app/component-library/components/Toast/ToastPerps.stories.tsx
new file mode 100644
index 00000000000..c18221ef08c
--- /dev/null
+++ b/app/component-library/components/Toast/ToastPerps.stories.tsx
@@ -0,0 +1,432 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import type { OrderDirection, Position } from '@metamask/perps-controller';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import usePerpsToasts, {
+ type PerpsToastOptions,
+ type PerpsToastOptionsConfig,
+} from '../../../components/UI/Perps/hooks/usePerpsToasts';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import { StoryContainer, StoryToastHost } from './ToastStory.shared';
+
+const MOCK_TRANSACTION_ID = 'c9a6ab70-8e70-11f0-8de9-353809172f0a';
+const MOCK_LONG = 'long' as OrderDirection;
+const MOCK_SHORT = 'short' as OrderDirection;
+const MOCK_ETH = 'ETH';
+const MOCK_BTC = 'BTC';
+const MOCK_AMOUNT = '1.5';
+
+const MOCK_FULL_POSITION = {
+ symbol: MOCK_ETH,
+ size: MOCK_AMOUNT,
+ unrealizedPnl: '100',
+ returnOnEquity: '0.15',
+} as Position;
+
+const MOCK_PARTIAL_POSITION = {
+ symbol: MOCK_BTC,
+ size: '-0.5',
+ unrealizedPnl: '50',
+ returnOnEquity: '0.08',
+} as Position;
+
+interface PerpsToastTrigger {
+ label: string;
+ getConfig: (options: PerpsToastOptionsConfig) => PerpsToastOptions;
+}
+
+interface PerpsToastTriggerSection {
+ title: string;
+ triggers: PerpsToastTrigger[];
+}
+
+const buildPerpsToastSections = (
+ options: PerpsToastOptionsConfig,
+): PerpsToastTriggerSection[] => [
+ {
+ title: 'Deposit',
+ triggers: [
+ {
+ label: 'Deposit success',
+ getConfig: () => options.accountManagement.deposit.success('100'),
+ },
+ {
+ label: 'Deposit in progress',
+ getConfig: () =>
+ options.accountManagement.deposit.inProgress(60, MOCK_TRANSACTION_ID),
+ },
+ {
+ label: 'Deposit taking longer',
+ getConfig: () => options.accountManagement.deposit.takingLonger,
+ },
+ {
+ label: 'Trade canceled',
+ getConfig: () => options.accountManagement.deposit.tradeCanceled,
+ },
+ {
+ label: 'Deposit error',
+ getConfig: () => options.accountManagement.deposit.error,
+ },
+ ],
+ },
+ {
+ title: 'One-click trade',
+ triggers: [
+ {
+ label: 'Tx creation failed',
+ getConfig: () =>
+ options.accountManagement.oneClickTrade.txCreationFailed,
+ },
+ ],
+ },
+ {
+ title: 'Withdrawal',
+ triggers: [
+ {
+ label: 'Withdrawal in progress',
+ getConfig: () =>
+ options.accountManagement.withdrawal.withdrawalInProgress,
+ },
+ {
+ label: 'Withdrawal success',
+ getConfig: () =>
+ options.accountManagement.withdrawal.withdrawalSuccess('101', 'USDC'),
+ },
+ {
+ label: 'Withdrawal failed',
+ getConfig: () =>
+ options.accountManagement.withdrawal.withdrawalFailed(
+ 'Insufficient balance',
+ ),
+ },
+ {
+ label: 'Withdrawal start failed',
+ getConfig: () =>
+ options.accountManagement.withdrawal.withdrawalStartFailed(() => {
+ Alert.alert('Retry pressed');
+ }),
+ },
+ ],
+ },
+ {
+ title: 'Market orders',
+ triggers: [
+ {
+ label: 'Market submitted',
+ getConfig: () =>
+ options.orderManagement.market.submitted(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Market confirmed',
+ getConfig: () =>
+ options.orderManagement.market.confirmed(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Market creation failed',
+ getConfig: () =>
+ options.orderManagement.market.creationFailed('Order rejected'),
+ },
+ ],
+ },
+ {
+ title: 'Limit orders',
+ triggers: [
+ {
+ label: 'Limit submitted',
+ getConfig: () =>
+ options.orderManagement.limit.submitted(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Limit confirmed',
+ getConfig: () =>
+ options.orderManagement.limit.confirmed(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Limit creation failed',
+ getConfig: () =>
+ options.orderManagement.limit.creationFailed('Insufficient margin'),
+ },
+ ],
+ },
+ {
+ title: 'Order shared',
+ triggers: [
+ {
+ label: 'Submitting',
+ getConfig: () => options.orderManagement.shared.submitting(),
+ },
+ {
+ label: 'Cancellation in progress',
+ getConfig: () =>
+ options.orderManagement.shared.cancellationInProgress(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ 'Limit',
+ ),
+ },
+ {
+ label: 'Cancellation success',
+ getConfig: () =>
+ options.orderManagement.shared.cancellationSuccess(
+ false,
+ 'Limit',
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Cancellation failed',
+ getConfig: () => options.orderManagement.shared.cancellationFailed,
+ },
+ ],
+ },
+ {
+ title: 'Close position — market',
+ triggers: [
+ {
+ label: 'Full close in progress',
+ getConfig: () =>
+ options.positionManagement.closePosition.marketClose.full.closeFullPositionInProgress(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Full close success',
+ getConfig: () =>
+ options.positionManagement.closePosition.marketClose.full.closeFullPositionSuccess(
+ MOCK_FULL_POSITION,
+ '3200',
+ ),
+ },
+ {
+ label: 'Full close failed',
+ getConfig: () =>
+ options.positionManagement.closePosition.marketClose.full
+ .closeFullPositionFailed,
+ },
+ {
+ label: 'Partial close in progress',
+ getConfig: () =>
+ options.positionManagement.closePosition.marketClose.partial.closePartialPositionInProgress(
+ MOCK_SHORT,
+ '-0.5',
+ MOCK_BTC,
+ ),
+ },
+ {
+ label: 'Partial close success',
+ getConfig: () =>
+ options.positionManagement.closePosition.marketClose.partial.closePartialPositionSuccess(
+ MOCK_PARTIAL_POSITION,
+ '98000',
+ ),
+ },
+ {
+ label: 'Partial close failed',
+ getConfig: () =>
+ options.positionManagement.closePosition.marketClose.partial
+ .closePartialPositionFailed,
+ },
+ ],
+ },
+ {
+ title: 'Close position — limit',
+ triggers: [
+ {
+ label: 'Full limit close submitted',
+ getConfig: () =>
+ options.positionManagement.closePosition.limitClose.full.fullPositionCloseSubmitted(
+ MOCK_LONG,
+ MOCK_AMOUNT,
+ MOCK_ETH,
+ ),
+ },
+ {
+ label: 'Partial limit close submitted',
+ getConfig: () =>
+ options.positionManagement.closePosition.limitClose.partial.partialPositionCloseSubmitted(
+ MOCK_SHORT,
+ '-0.5',
+ MOCK_BTC,
+ ),
+ },
+ {
+ label: 'Missing limit price',
+ getConfig: () =>
+ options.positionManagement.closePosition.limitClose.partial
+ .switchToMarketOrderMissingLimitPrice,
+ },
+ ],
+ },
+ {
+ title: 'TP/SL',
+ triggers: [
+ {
+ label: 'Update success',
+ getConfig: () => options.positionManagement.tpsl.updateTPSLSuccess,
+ },
+ {
+ label: 'Update error',
+ getConfig: () =>
+ options.positionManagement.tpsl.updateTPSLError('Invalid price'),
+ },
+ ],
+ },
+ {
+ title: 'Margin',
+ triggers: [
+ {
+ label: 'Add success',
+ getConfig: () =>
+ options.positionManagement.margin.addSuccess(MOCK_ETH, '50'),
+ },
+ {
+ label: 'Remove success',
+ getConfig: () =>
+ options.positionManagement.margin.removeSuccess(MOCK_ETH, '25'),
+ },
+ {
+ label: 'Adjustment failed',
+ getConfig: () =>
+ options.positionManagement.margin.adjustmentFailed('Margin too low'),
+ },
+ ],
+ },
+ {
+ title: 'Form validation',
+ triggers: [
+ {
+ label: 'Validation error',
+ getConfig: () =>
+ options.formValidation.orderForm.validationError(
+ 'Minimum order size is 0.01 ETH',
+ ),
+ },
+ {
+ label: 'Limit price required',
+ getConfig: () => options.formValidation.orderForm.limitPriceRequired,
+ },
+ ],
+ },
+ {
+ title: 'Data fetching',
+ triggers: [
+ {
+ label: 'Market data unavailable',
+ getConfig: () =>
+ options.dataFetching.market.error.marketDataUnavailable(MOCK_ETH),
+ },
+ ],
+ },
+ {
+ title: 'Content sharing',
+ triggers: [
+ {
+ label: 'PnL share success',
+ getConfig: () => options.contentSharing.pnlHeroCard.shareSuccess,
+ },
+ {
+ label: 'PnL share failed',
+ getConfig: () => options.contentSharing.pnlHeroCard.shareFailed,
+ },
+ ],
+ },
+ {
+ title: 'Watchlist',
+ triggers: [
+ {
+ label: 'Add error',
+ getConfig: () => options.watchlist.addError,
+ },
+ {
+ label: 'Limit reached',
+ getConfig: () => options.watchlist.limitReached,
+ },
+ ],
+ },
+];
+
+const PerpsToastsStoryContent = () => {
+ const tw = useTailwind();
+ const { toastRef } = useContext(ToastContext);
+ const { showToast, PerpsToastOptions } = usePerpsToasts();
+ const sections = useMemo(
+ () => buildPerpsToastSections(PerpsToastOptions),
+ [PerpsToastOptions],
+ );
+
+ const triggerPerpsToast = useCallback(
+ (trigger: PerpsToastTrigger) => {
+ showToast({
+ ...trigger.getConfig(PerpsToastOptions),
+ hasNoTimeout: true,
+ });
+ },
+ [PerpsToastOptions, showToast],
+ );
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+
+ ))}
+
+
+
+ );
+};
+
+const PerpsToastsStory = () => (
+
+
+
+);
+
+const PerpsToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default PerpsToastsMeta;
+
+export const PerpsToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastPerpsWithdraw.stories.tsx b/app/component-library/components/Toast/ToastPerpsWithdraw.stories.tsx
new file mode 100644
index 00000000000..4fa4f2952dc
--- /dev/null
+++ b/app/component-library/components/Toast/ToastPerpsWithdraw.stories.tsx
@@ -0,0 +1,208 @@
+import React, { useCallback, useContext, useMemo, type RefObject } from 'react';
+import { ScrollView, View } from 'react-native';
+import {
+ TransactionMeta,
+ TransactionStatus,
+ TransactionType,
+} from '@metamask/transaction-controller';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import { usePerpsWithdrawToastRegistrations } from '../../../components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations';
+import { strings } from '../../../../locales/i18n';
+import { IconName } from '../Icons/Icon';
+import Toast from './Toast';
+import { ToastVariants, type ToastOptions, type ToastRef } from './Toast.types';
+import { useAppThemeFromContext } from '../../../util/theme';
+import type { Theme } from '../../../util/theme/models';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import {
+ presentStoryToast,
+ StoryContainer,
+ StoryToastHost,
+} from './ToastStory.shared';
+
+const MOCK_PENDING_TX_ID = 'perps-withdraw-story-pending';
+const MOCK_CONFIRMED_TX_ID = 'perps-withdraw-story-confirmed';
+const MOCK_FAILED_TX_ID = 'perps-withdraw-story-failed';
+
+interface PerpsWithdrawHandlerTrigger {
+ label: string;
+ transactionId: string;
+ status: TransactionStatus;
+}
+
+interface PerpsWithdrawDescriptionTrigger {
+ label: string;
+ description: string;
+}
+
+interface PerpsWithdrawToastTriggerSection {
+ title: string;
+ handlerTriggers?: PerpsWithdrawHandlerTrigger[];
+ descriptionTriggers?: PerpsWithdrawDescriptionTrigger[];
+}
+
+const PERPS_WITHDRAW_TOAST_SECTIONS: PerpsWithdrawToastTriggerSection[] = [
+ {
+ title: 'Lifecycle',
+ handlerTriggers: [
+ {
+ label: 'Pending',
+ transactionId: MOCK_PENDING_TX_ID,
+ status: TransactionStatus.approved,
+ },
+ {
+ label: 'Confirmed (generic)',
+ transactionId: MOCK_CONFIRMED_TX_ID,
+ status: TransactionStatus.confirmed,
+ },
+ {
+ label: 'Failed',
+ transactionId: MOCK_FAILED_TX_ID,
+ status: TransactionStatus.failed,
+ },
+ ],
+ },
+ {
+ title: 'Confirmed descriptions',
+ descriptionTriggers: [
+ {
+ label: 'Generic',
+ description: strings(
+ 'perps.withdrawal.toast_completed_subtitle_generic',
+ ),
+ },
+ {
+ label: 'Post-quote token',
+ description: strings(
+ 'perps.withdrawal.toast_completed_any_token_subtitle',
+ {
+ amount: '$0.25',
+ token: 'BNB',
+ },
+ ),
+ },
+ {
+ label: 'Post-quote ticker fallback',
+ description: strings(
+ 'perps.withdrawal.toast_completed_any_token_subtitle',
+ {
+ amount: '$1.50',
+ token: 'ETH',
+ },
+ ),
+ },
+ ],
+ },
+];
+
+const buildConfirmedWithdrawToast = (
+ theme: Theme,
+ description: string,
+): ToastOptions => ({
+ variant: ToastVariants.Icon,
+ labelOptions: [
+ {
+ label: strings('perps.withdrawal.toast_completed_title'),
+ isBold: true,
+ },
+ { label: '\n', isBold: false },
+ { label: description, isBold: false },
+ ],
+ iconName: IconName.Confirmation,
+ iconColor: theme.colors.success.default,
+ hasNoTimeout: false,
+});
+
+const createStoryShowToast =
+ (toastRef: RefObject | undefined): ToastRef['showToast'] =>
+ (options) => {
+ presentStoryToast(toastRef, options);
+ };
+
+const PerpsWithdrawToastsStoryContent = () => {
+ const tw = useTailwind();
+ const theme = useAppThemeFromContext();
+ const { toastRef } = useContext(ToastContext);
+ const registrations = usePerpsWithdrawToastRegistrations();
+ const handler = registrations[0]?.handler;
+
+ const triggerHandlerToast = useCallback(
+ (trigger: PerpsWithdrawHandlerTrigger) => {
+ handler?.(
+ {
+ transactionMeta: {
+ id: trigger.transactionId,
+ type: TransactionType.perpsWithdraw,
+ status: trigger.status,
+ } as TransactionMeta,
+ },
+ createStoryShowToast(toastRef),
+ );
+ },
+ [handler, toastRef],
+ );
+
+ const triggerDescriptionToast = useCallback(
+ (trigger: PerpsWithdrawDescriptionTrigger) => {
+ presentStoryToast(
+ toastRef,
+ buildConfirmedWithdrawToast(theme, trigger.description),
+ );
+ },
+ [theme, toastRef],
+ );
+
+ const sections = useMemo(() => PERPS_WITHDRAW_TOAST_SECTIONS, []);
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.handlerTriggers?.map((trigger) => (
+
+ ))}
+
+
+
+ );
+};
+
+const PerpsWithdrawToastsStory = () => (
+
+
+
+);
+
+const PerpsWithdrawToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default PerpsWithdrawToastsMeta;
+
+export const PerpsWithdrawToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastPredict.stories.tsx b/app/component-library/components/Toast/ToastPredict.stories.tsx
new file mode 100644
index 00000000000..19cf9deb3ee
--- /dev/null
+++ b/app/component-library/components/Toast/ToastPredict.stories.tsx
@@ -0,0 +1,263 @@
+import React, { useCallback, useContext, useMemo, type RefObject } from 'react';
+import { ScrollView, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import { createPredictTransactionStatusChangedHandler } from '../../../components/UI/Predict/hooks/predictTransactionStatusChangedToastHandler';
+import type { PredictTransactionStatusChangedPayload } from '../../../components/UI/Predict/controllers/PredictController';
+import { formatPrice } from '../../../components/UI/Predict/utils/format';
+import { strings } from '../../../../locales/i18n';
+import { useAppThemeFromContext } from '../../../util/theme';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import type { ToastRef } from './Toast.types';
+import {
+ presentStoryToast,
+ StoryContainer,
+ StoryToastHost,
+} from './ToastStory.shared';
+
+const MOCK_TRANSACTION_ID = 'c9a6ab70-8e70-11f0-8de9-353809172f0a';
+const MOCK_SENDER_ADDRESS = '0x2990079bcdEe240329a520d2444386FC119da21a';
+
+interface PredictToastTrigger {
+ label: string;
+ payload: PredictTransactionStatusChangedPayload;
+}
+
+interface PredictToastTriggerSection {
+ title: string;
+ triggers: PredictToastTrigger[];
+}
+
+const PREDICT_TOAST_SECTIONS: PredictToastTriggerSection[] = [
+ {
+ title: 'Deposit',
+ triggers: [
+ {
+ label: 'Deposit approved',
+ payload: {
+ type: 'deposit',
+ status: 'approved',
+ transactionId: MOCK_TRANSACTION_ID,
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Deposit confirmed',
+ payload: {
+ type: 'deposit',
+ status: 'confirmed',
+ amount: 102,
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Deposit confirmed (no amount)',
+ payload: {
+ type: 'deposit',
+ status: 'confirmed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Deposit failed',
+ payload: {
+ type: 'deposit',
+ status: 'failed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ ],
+ },
+ {
+ title: 'Claim',
+ triggers: [
+ {
+ label: 'Claim approved',
+ payload: {
+ type: 'claim',
+ status: 'approved',
+ amount: 55.12,
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Claim confirmed',
+ payload: {
+ type: 'claim',
+ status: 'confirmed',
+ amount: 45.5,
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Claim failed',
+ payload: {
+ type: 'claim',
+ status: 'failed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ ],
+ },
+ {
+ title: 'Withdraw',
+ triggers: [
+ {
+ label: 'Withdraw approved',
+ payload: {
+ type: 'withdraw',
+ status: 'approved',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Withdraw confirmed',
+ payload: {
+ type: 'withdraw',
+ status: 'confirmed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ amount: 55.12,
+ transactionId: MOCK_TRANSACTION_ID,
+ },
+ },
+ {
+ label: 'Withdraw failed',
+ payload: {
+ type: 'withdraw',
+ status: 'failed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ ],
+ },
+ {
+ title: 'Order',
+ triggers: [
+ {
+ label: 'Order depositing',
+ payload: {
+ type: 'order',
+ status: 'depositing',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Order confirmed',
+ payload: {
+ type: 'order',
+ status: 'confirmed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ {
+ label: 'Order failed',
+ payload: {
+ type: 'order',
+ status: 'failed',
+ senderAddress: MOCK_SENDER_ADDRESS,
+ },
+ },
+ ],
+ },
+];
+
+const predictStoryQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+const createStoryShowToast =
+ (toastRef: RefObject | undefined): ToastRef['showToast'] =>
+ (options) => {
+ presentStoryToast(toastRef, options);
+ };
+
+const PredictToastsStoryContent = () => {
+ const tw = useTailwind();
+ const navigation = useNavigation();
+ const theme = useAppThemeFromContext();
+ const { toastRef } = useContext(ToastContext);
+ const handler = useMemo(
+ () =>
+ createPredictTransactionStatusChangedHandler({
+ queryClient: predictStoryQueryClient,
+ navigation,
+ theme,
+ normalizedSelectedAddress: MOCK_SENDER_ADDRESS.toLowerCase(),
+ bottomSheetEnabled: false,
+ deposit: () => Promise.resolve(),
+ claim: () => Promise.resolve(),
+ withdraw: () => Promise.resolve(),
+ withdrawTransactionAmount: 123.45,
+ getTransactionMetadata: () => undefined,
+ isPerpsPredictMoneyDeposit: () => false,
+ isPerpsPredictMoneyWithdraw: () => false,
+ shouldSuppressLegacyOrderFailureToast: () => false,
+ getWithdrawConfirmedMessage: (_transactionId, fallbackAmount) => ({
+ title: strings('predict.withdraw.withdraw_completed'),
+ description: strings('predict.withdraw.withdraw_completed_subtitle', {
+ amount: formatPrice(fallbackAmount),
+ }),
+ }),
+ }),
+ [navigation, theme],
+ );
+
+ const triggerPredictToast = useCallback(
+ (trigger: PredictToastTrigger) => {
+ handler(trigger.payload, createStoryShowToast(toastRef));
+ },
+ [handler, toastRef],
+ );
+
+ return (
+
+
+ {PREDICT_TOAST_SECTIONS.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+ triggerPredictToast(trigger)}
+ />
+ ))}
+
+ ))}
+
+
+
+ );
+};
+
+const PredictToastsStory = () => (
+
+
+
+
+
+);
+
+const PredictToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default PredictToastsMeta;
+
+export const PredictToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastQuickBuy.stories.tsx b/app/component-library/components/Toast/ToastQuickBuy.stories.tsx
new file mode 100644
index 00000000000..436fce4f85c
--- /dev/null
+++ b/app/component-library/components/Toast/ToastQuickBuy.stories.tsx
@@ -0,0 +1,149 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import { ScrollView, View } from 'react-native';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import {
+ buildQuickBuyToastOptions,
+ type QuickBuyToastKind,
+} from '../../../components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyToastOptions';
+import type { TrackedQuickBuyTrade } from '../../../components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker';
+import { useAppThemeFromContext } from '../../../util/theme';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import type { ToastOptions } from './Toast.types';
+import {
+ presentStoryToast,
+ StoryContainer,
+ StoryToastHost,
+} from './ToastStory.shared';
+
+const MOCK_BUY_TRADE: TrackedQuickBuyTrade = {
+ tradeMode: 'buy',
+ tokenSymbol: 'PEPE',
+ counterTokenSymbol: 'USDC',
+ fiatAmountLabel: '$50.00',
+ rate: '1 USDC = 1,000 PEPE',
+};
+
+const MOCK_SELL_TRADE: TrackedQuickBuyTrade = {
+ tradeMode: 'sell',
+ tokenSymbol: 'DOGE',
+ counterTokenSymbol: 'USDC',
+ fiatAmountLabel: '$25.00',
+};
+
+interface QuickBuyToastTrigger {
+ label: string;
+ kind: QuickBuyToastKind;
+ trade: TrackedQuickBuyTrade;
+}
+
+interface QuickBuyToastTriggerSection {
+ title: string;
+ triggers: QuickBuyToastTrigger[];
+}
+
+const QUICK_BUY_TOAST_SECTIONS: QuickBuyToastTriggerSection[] = [
+ {
+ title: 'Buy',
+ triggers: [
+ {
+ label: 'Pending',
+ kind: 'pending',
+ trade: MOCK_BUY_TRADE,
+ },
+ {
+ label: 'Complete',
+ kind: 'complete',
+ trade: MOCK_BUY_TRADE,
+ },
+ {
+ label: 'Failed',
+ kind: 'failed',
+ trade: MOCK_BUY_TRADE,
+ },
+ ],
+ },
+ {
+ title: 'Sell',
+ triggers: [
+ {
+ label: 'Pending',
+ kind: 'pending',
+ trade: MOCK_SELL_TRADE,
+ },
+ {
+ label: 'Complete',
+ kind: 'complete',
+ trade: MOCK_SELL_TRADE,
+ },
+ {
+ label: 'Failed (no rate)',
+ kind: 'failed',
+ trade: MOCK_SELL_TRADE,
+ },
+ ],
+ },
+];
+
+const QuickBuyToastsStoryContent = () => {
+ const tw = useTailwind();
+ const theme = useAppThemeFromContext();
+ const { toastRef } = useContext(ToastContext);
+
+ const triggerQuickBuyToast = useCallback(
+ (trigger: QuickBuyToastTrigger) => {
+ const options: ToastOptions = buildQuickBuyToastOptions(trigger.kind, {
+ trade: trigger.trade,
+ theme,
+ });
+ presentStoryToast(toastRef, options);
+ },
+ [theme, toastRef],
+ );
+
+ const sections = useMemo(() => QUICK_BUY_TOAST_SECTIONS, []);
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+ triggerQuickBuyToast(trigger)}
+ />
+ ))}
+
+ ))}
+
+
+
+ );
+};
+
+const QuickBuyToastsStory = () => (
+
+
+
+);
+
+const QuickBuyToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default QuickBuyToastsMeta;
+
+export const QuickBuyToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastRamp.stories.tsx b/app/component-library/components/Toast/ToastRamp.stories.tsx
new file mode 100644
index 00000000000..7246cef8a94
--- /dev/null
+++ b/app/component-library/components/Toast/ToastRamp.stories.tsx
@@ -0,0 +1,155 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import { RampsOrderStatus } from '@metamask/ramps-controller';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import {
+ buildV2OrderToastOptions,
+ type V2OrderToastParams,
+} from '../../../components/UI/Ramp/utils/v2OrderToast';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import type { ToastOptions } from './Toast.types';
+import {
+ presentStoryToast,
+ StoryContainer,
+ StoryToastHost,
+} from './ToastStory.shared';
+
+const MOCK_ORDER_ID = 'ramp-story-order-001';
+const MOCK_CRYPTOCURRENCY = 'ETH';
+const MOCK_CRYPTO_AMOUNT = '1.5';
+
+interface RampToastTrigger {
+ label: string;
+ params: V2OrderToastParams;
+}
+
+const RAMP_TOAST_TRIGGERS: RampToastTrigger[] = [
+ {
+ label: 'Pending',
+ params: {
+ orderId: MOCK_ORDER_ID,
+ cryptocurrency: MOCK_CRYPTOCURRENCY,
+ cryptoAmount: MOCK_CRYPTO_AMOUNT,
+ status: RampsOrderStatus.Pending,
+ },
+ },
+ {
+ label: 'Completed',
+ params: {
+ orderId: MOCK_ORDER_ID,
+ cryptocurrency: MOCK_CRYPTOCURRENCY,
+ cryptoAmount: MOCK_CRYPTO_AMOUNT,
+ status: RampsOrderStatus.Completed,
+ },
+ },
+ {
+ label: 'Completed (no amount)',
+ params: {
+ orderId: MOCK_ORDER_ID,
+ cryptocurrency: MOCK_CRYPTOCURRENCY,
+ status: RampsOrderStatus.Completed,
+ },
+ },
+ {
+ label: 'Failed',
+ params: {
+ orderId: MOCK_ORDER_ID,
+ cryptocurrency: MOCK_CRYPTOCURRENCY,
+ status: RampsOrderStatus.Failed,
+ },
+ },
+ {
+ label: 'Cancelled',
+ params: {
+ orderId: MOCK_ORDER_ID,
+ cryptocurrency: MOCK_CRYPTOCURRENCY,
+ status: RampsOrderStatus.Cancelled,
+ },
+ },
+];
+
+const toStoryRampToastOptions = (
+ options: ToastOptions | null,
+): ToastOptions | null => {
+ if (!options) {
+ return null;
+ }
+
+ if (!options.linkButtonOptions) {
+ return options;
+ }
+
+ return {
+ ...options,
+ linkButtonOptions: {
+ ...options.linkButtonOptions,
+ onPress: () => Alert.alert('Storybook', 'Track order pressed'),
+ },
+ };
+};
+
+const RampToastsStoryContent = () => {
+ const tw = useTailwind();
+ const { toastRef } = useContext(ToastContext);
+
+ const triggerRampToast = useCallback(
+ (trigger: RampToastTrigger) => {
+ const options = toStoryRampToastOptions(
+ buildV2OrderToastOptions(trigger.params),
+ );
+
+ if (!options) {
+ Alert.alert('Storybook', 'This status does not produce a toast.');
+ return;
+ }
+
+ presentStoryToast(toastRef, options);
+ },
+ [toastRef],
+ );
+
+ const triggers = useMemo(() => RAMP_TOAST_TRIGGERS, []);
+
+ return (
+
+
+
+ Ramps V2 orders
+ {triggers.map((trigger) => (
+ triggerRampToast(trigger)}
+ />
+ ))}
+
+
+
+
+ );
+};
+
+const RampToastsStory = () => (
+
+
+
+);
+
+const RampToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default RampToastsMeta;
+
+export const RampToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastRewards.stories.tsx b/app/component-library/components/Toast/ToastRewards.stories.tsx
new file mode 100644
index 00000000000..ddd37861905
--- /dev/null
+++ b/app/component-library/components/Toast/ToastRewards.stories.tsx
@@ -0,0 +1,209 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import useRewardsToast, {
+ type RewardsToastConfig,
+ type RewardsToastOptions,
+} from '../../../components/UI/Rewards/hooks/useRewardsToast';
+import { strings } from '../../../../locales/i18n';
+import Button, { ButtonVariants } from '../Buttons/Button';
+import Toast from './Toast';
+import { ToastContext, ToastContextWrapper } from './Toast.context';
+import { StoryContainer, StoryToastHost } from './ToastStory.shared';
+
+const MOCK_CAMPAIGN_NAME = 'Summer Swap';
+
+const showStoryAlert = (message: string) => {
+ Alert.alert('Storybook', message);
+};
+
+interface RewardsToastTrigger {
+ label: string;
+ getConfig: (options: RewardsToastConfig) => RewardsToastOptions;
+}
+
+interface RewardsToastTriggerSection {
+ title: string;
+ triggers: RewardsToastTrigger[];
+}
+
+const buildRewardsToastSections = (
+ options: RewardsToastConfig,
+): RewardsToastTriggerSection[] => [
+ {
+ title: 'Base',
+ triggers: [
+ {
+ label: 'Success',
+ getConfig: () =>
+ options.success(strings('rewards.notifications_nudge.success')),
+ },
+ {
+ label: 'Success with description',
+ getConfig: () =>
+ options.success(
+ strings('rewards.campaign.remind_me_success_toast'),
+ 'You can manage reminders from campaign settings.',
+ ),
+ },
+ {
+ label: 'Error',
+ getConfig: () =>
+ options.error(strings('rewards.optout.modal.error_message')),
+ },
+ {
+ label: 'Error with description',
+ getConfig: () =>
+ options.error(
+ strings('rewards.optin_error.title'),
+ strings('rewards.optin_error.description'),
+ ),
+ },
+ {
+ label: 'Loading',
+ getConfig: () =>
+ options.loading(
+ strings('rewards.notifications_nudge.loading'),
+ strings('rewards.notifications_nudge.loading_description'),
+ ),
+ },
+ {
+ label: 'Warning',
+ getConfig: () =>
+ options.warning(
+ strings('rewards.optout.request_received.title'),
+ strings('rewards.optout.request_received.description'),
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Campaign',
+ triggers: [
+ {
+ label: 'Entries closed',
+ getConfig: () =>
+ options.entriesClosed(
+ strings('rewards.campaign_details.ondo.entries_closed_title'),
+ ),
+ },
+ {
+ label: 'Entries closed with description',
+ getConfig: () =>
+ options.entriesClosed(
+ strings('rewards.campaign_details.ondo.entries_closed_title'),
+ strings('rewards.campaign_details.ondo.entries_closed_description'),
+ ),
+ },
+ ],
+ },
+ {
+ title: 'Notifications',
+ triggers: [
+ {
+ label: 'Enable notifications nudge',
+ getConfig: () =>
+ options.enableNotificationsNudge({
+ label: strings('rewards.notifications_nudge.turn_on_button'),
+ onPress: () => showStoryAlert('Turn on notifications pressed'),
+ }),
+ },
+ ],
+ },
+ {
+ title: 'Campaign outcome',
+ triggers: [
+ {
+ label: 'Outcome winner',
+ getConfig: () =>
+ options.outcomeWinner({
+ title: strings('rewards.campaign_outcome_toast.winner.title'),
+ description: strings(
+ 'rewards.campaign_outcome_toast.winner.description',
+ { campaignName: MOCK_CAMPAIGN_NAME },
+ ),
+ ctaLabel: strings('rewards.campaign_outcome_toast.winner.cta'),
+ onCtaPress: () => showStoryAlert('View details pressed'),
+ onClosePress: () => showStoryAlert('Outcome toast dismissed'),
+ }),
+ },
+ {
+ label: 'Outcome non-winner',
+ getConfig: () =>
+ options.outcomeNonWinner({
+ title: strings('rewards.campaign_outcome_toast.non_winner.title'),
+ description: strings(
+ 'rewards.campaign_outcome_toast.non_winner.description',
+ { campaignName: MOCK_CAMPAIGN_NAME },
+ ),
+ ctaLabel: strings('rewards.campaign_outcome_toast.non_winner.cta'),
+ onCtaPress: () => showStoryAlert('View details pressed'),
+ onClosePress: () => showStoryAlert('Outcome toast dismissed'),
+ }),
+ },
+ ],
+ },
+];
+
+const RewardsToastsStoryContent = () => {
+ const tw = useTailwind();
+ const { toastRef } = useContext(ToastContext);
+ const { showToast, RewardsToastOptions } = useRewardsToast();
+ const sections = useMemo(
+ () => buildRewardsToastSections(RewardsToastOptions),
+ [RewardsToastOptions],
+ );
+
+ const triggerRewardsToast = useCallback(
+ (trigger: RewardsToastTrigger) => {
+ showToast({
+ ...trigger.getConfig(RewardsToastOptions),
+ hasNoTimeout: true,
+ });
+ },
+ [RewardsToastOptions, showToast],
+ );
+
+ return (
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {section.triggers.map((trigger) => (
+ triggerRewardsToast(trigger)}
+ />
+ ))}
+
+ ))}
+
+
+
+ );
+};
+
+const RewardsToastsStory = () => (
+
+
+
+);
+
+const RewardsToastsMeta = {
+ title: 'Component Library / Toast',
+ component: Toast,
+};
+
+export default RewardsToastsMeta;
+
+export const RewardsToasts = {
+ render: () => ,
+};
diff --git a/app/component-library/components/Toast/ToastStory.shared.tsx b/app/component-library/components/Toast/ToastStory.shared.tsx
new file mode 100644
index 00000000000..278487a4998
--- /dev/null
+++ b/app/component-library/components/Toast/ToastStory.shared.tsx
@@ -0,0 +1,47 @@
+import React, { type RefObject } from 'react';
+import { View } from 'react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import Toast from './Toast';
+import type { ToastOptions, ToastRef } from './Toast.types';
+
+export const STORYBOOK_TOAST_OPTIONS: Pick = {
+ hasNoTimeout: true,
+};
+
+export const presentStoryToast = (
+ toastRef: RefObject | undefined,
+ options: ToastOptions,
+) => {
+ toastRef?.current?.showToast({
+ ...options,
+ hasNoTimeout: true,
+ });
+};
+
+export const StoryContainer = ({ children }: { children: React.ReactNode }) => {
+ const tw = useTailwind();
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const StoryToastHost = ({
+ toastRef,
+}: {
+ toastRef: RefObject | undefined;
+}) => {
+ const tw = useTailwind();
+
+ return (
+
+
+
+ );
+};
diff --git a/app/components/UI/Earn/hooks/useEarnToasts.tsx b/app/components/UI/Earn/hooks/useEarnToasts.tsx
index b7e6aefbed7..d544e8f0744 100644
--- a/app/components/UI/Earn/hooks/useEarnToasts.tsx
+++ b/app/components/UI/Earn/hooks/useEarnToasts.tsx
@@ -4,7 +4,6 @@ import {
type HapticNotificationMoment,
} from '../../../../util/haptics';
import React, { useCallback, useContext, useMemo } from 'react';
-import { StyleSheet, View } from 'react-native';
import { strings } from '../../../../../locales/i18n';
import Icon, {
IconName,
@@ -19,6 +18,7 @@ import {
import { useAppThemeFromContext } from '../../../../util/theme';
import {
Spinner,
+ IconColor,
IconSize as ReactNativeDsIconSize,
Text,
TextColor,
@@ -66,7 +66,7 @@ interface EarnToastLabelOptions {
const getEarnToastLabels = ({
primary,
secondary,
- primaryIsBold = false,
+ primaryIsBold = true,
}: EarnToastLabelOptions) => {
const labels = [
{
@@ -95,12 +95,6 @@ const EARN_TOASTS_DEFAULT_OPTIONS: Partial = {
hasNoTimeout: false,
};
-const toastStyles = StyleSheet.create({
- iconWrapper: {
- marginRight: 16,
- },
-});
-
const useEarnToasts = (): {
showToast: (config: EarnToastOptions) => void;
EarnToastOptions: EarnToastOptionsConfig;
@@ -130,13 +124,11 @@ const useEarnToasts = (): {
iconColor: theme.colors.success.default,
hapticsType: NotificationMoment.Success,
startAccessory: (
-
-
-
+
),
},
inProgress: {
@@ -146,9 +138,10 @@ const useEarnToasts = (): {
hapticsType: NotificationMoment.Warning,
hasNoTimeout: true,
startAccessory: (
-
-
-
+
),
},
error: {
@@ -158,13 +151,11 @@ const useEarnToasts = (): {
iconColor: theme.colors.error.default,
hapticsType: NotificationMoment.Error,
startAccessory: (
-
-
-
+
),
},
}),
@@ -245,7 +236,6 @@ const useEarnToasts = (): {
...earnBaseToastOptions.error,
labelOptions: getEarnToastLabels({
primary: strings('stake.tron.unstaked_banner.error'),
- primaryIsBold: true,
...(errors.length > 0 && {
secondary: (
= {
hasNoTimeout: false,
};
-const toastStyles = StyleSheet.create({
- iconWrapper: {
- marginRight: 16,
- },
-});
-
const useMoneyToasts = (): {
showToast: (config: MoneyToastOptions) => void;
MoneyToastOptions: MoneyToastOptionsConfig;
@@ -170,13 +163,11 @@ const useMoneyToasts = (): {
iconColor: theme.colors.success.default,
hapticsType: NotificationMoment.Success,
startAccessory: (
-
-
-
+
),
},
inProgress: {
@@ -186,9 +177,7 @@ const useMoneyToasts = (): {
hapticsType: NotificationMoment.Warning,
hasNoTimeout: true,
startAccessory: (
-
-
-
+
),
},
error: {
@@ -198,13 +187,11 @@ const useMoneyToasts = (): {
iconColor: theme.colors.error.default,
hapticsType: NotificationMoment.Error,
startAccessory: (
-
-
-
+
),
},
}),
diff --git a/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.stories.tsx b/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.stories.tsx
new file mode 100644
index 00000000000..1a99d21ba91
--- /dev/null
+++ b/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.stories.tsx
@@ -0,0 +1,162 @@
+import React, { useCallback, useState } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import Button, {
+ ButtonVariants,
+} from '../../../component-library/components/Buttons/Button';
+import type { NetworkConnectionBannerState } from '../../../reducers/networkConnectionBanner';
+import {
+ NetworkConnectionBannerView,
+ type NetworkConnectionBannerViewProps,
+} from './NetworkConnectionBanner';
+import type { NetworkConnectionBannerStatus } from './types';
+
+type VisibleNetworkConnectionBannerState = Exclude<
+ NetworkConnectionBannerState,
+ { visible: false }
+>;
+
+interface NetworkConnectionBannerTrigger {
+ label: string;
+ state: VisibleNetworkConnectionBannerState;
+}
+
+const MOCK_POLYGON_STATE = {
+ chainId: '0x89' as const,
+ networkName: 'Polygon Mainnet',
+ rpcUrl: 'https://polygon-rpc.com',
+};
+
+const MOCK_ETHEREUM_STATE = {
+ chainId: '0x1' as const,
+ networkName: 'Ethereum Mainnet',
+ rpcUrl: 'https://mainnet.infura.io/v3/test',
+};
+
+const buildVisibleState = (
+ status: NetworkConnectionBannerStatus,
+ overrides: Partial = {},
+): VisibleNetworkConnectionBannerState => ({
+ visible: true,
+ status,
+ isInfuraEndpoint: false,
+ ...MOCK_POLYGON_STATE,
+ ...overrides,
+});
+
+const NETWORK_CONNECTION_BANNER_TRIGGERS: NetworkConnectionBannerTrigger[] = [
+ {
+ label: 'Degraded — custom RPC (update)',
+ state: buildVisibleState('degraded'),
+ },
+ {
+ label: 'Degraded — custom RPC (switch to Infura)',
+ state: buildVisibleState('degraded', {
+ infuraNetworkClientId: 'infura-mainnet',
+ }),
+ },
+ {
+ label: 'Degraded — Infura endpoint',
+ state: buildVisibleState('degraded', {
+ ...MOCK_ETHEREUM_STATE,
+ isInfuraEndpoint: true,
+ }),
+ },
+ {
+ label: 'Unavailable — Infura endpoint',
+ state: buildVisibleState('unavailable', {
+ ...MOCK_ETHEREUM_STATE,
+ isInfuraEndpoint: true,
+ }),
+ },
+ {
+ label: 'Unavailable — custom RPC (update)',
+ state: buildVisibleState('unavailable'),
+ },
+ {
+ label: 'Unavailable — custom RPC (switch to Infura)',
+ state: buildVisibleState('unavailable', {
+ infuraNetworkClientId: 'infura-mainnet',
+ }),
+ },
+ {
+ label: 'Unavailable — long network name',
+ state: buildVisibleState('unavailable', {
+ networkName: 'Monad Mainnet YOYOMI JOK.OK.OK.OK.OK.OK.OK.OK',
+ }),
+ },
+];
+
+const StoryContainer = ({ children }: { children: React.ReactNode }) => {
+ const tw = useTailwind();
+
+ return (
+
+ {children}
+
+ );
+};
+
+const NetworkConnectionBannerStoryContent = () => {
+ const tw = useTailwind();
+ const [activeState, setActiveState] =
+ useState(null);
+
+ const showStoryAlert = useCallback((message: string) => {
+ Alert.alert('Storybook', message);
+ }, []);
+
+ const bannerProps: NetworkConnectionBannerViewProps | null = activeState
+ ? {
+ networkConnectionBannerState: activeState,
+ onUpdateRpc: () => showStoryAlert('Update RPC pressed'),
+ onSwitchToInfura: () => {
+ showStoryAlert('Switch to MetaMask default RPC pressed');
+ },
+ }
+ : null;
+
+ return (
+
+
+
+ Network connection banner
+ {NETWORK_CONNECTION_BANNER_TRIGGERS.map((trigger) => (
+ setActiveState(trigger.state)}
+ />
+ ))}
+ setActiveState(null)}
+ />
+
+ {bannerProps ? (
+
+
+
+ ) : null}
+
+
+ );
+};
+
+const NetworkConnectionBannerMeta = {
+ title: 'Components / UI / NetworkConnectionBanner',
+ component: NetworkConnectionBannerView,
+};
+
+export default NetworkConnectionBannerMeta;
+
+export const Default = {
+ render: () => ,
+};
diff --git a/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.tsx b/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.tsx
index e3319248b59..461399ac07d 100644
--- a/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.tsx
+++ b/app/components/UI/NetworkConnectionBanner/NetworkConnectionBanner.tsx
@@ -303,32 +303,29 @@ const getBannerContent = (
};
};
-export const NetworkConnectionBanner = () => {
+export interface NetworkConnectionBannerViewProps {
+ networkConnectionBannerState: Exclude<
+ NetworkConnectionBannerState,
+ { visible: false }
+ >;
+ onUpdateRpc: () => void;
+ onSwitchToInfura: () => void | Promise;
+}
+
+export const NetworkConnectionBannerView = ({
+ networkConnectionBannerState,
+ onUpdateRpc,
+ onSwitchToInfura,
+}: NetworkConnectionBannerViewProps) => {
const theme = useAppTheme();
const tw = useTailwind();
- const { networkConnectionBannerState, updateRpc, switchToInfura } =
- useNetworkConnectionBanner();
-
- const handleUpdateRpc = useCallback(() => {
- if (networkConnectionBannerState.visible) {
- updateRpc(
- networkConnectionBannerState.rpcUrl,
- networkConnectionBannerState.status,
- networkConnectionBannerState.chainId,
- );
- }
- }, [networkConnectionBannerState, updateRpc]);
-
- if (!networkConnectionBannerState.visible) {
- return null;
- }
const { primaryMessage, secondaryMessage, backgroundColor, icon } =
getBannerContent(
theme,
networkConnectionBannerState,
- handleUpdateRpc,
- switchToInfura,
+ onUpdateRpc,
+ onSwitchToInfura,
);
return (
@@ -349,4 +346,31 @@ export const NetworkConnectionBanner = () => {
);
};
+export const NetworkConnectionBanner = () => {
+ const { networkConnectionBannerState, updateRpc, switchToInfura } =
+ useNetworkConnectionBanner();
+
+ const handleUpdateRpc = useCallback(() => {
+ if (networkConnectionBannerState.visible) {
+ updateRpc(
+ networkConnectionBannerState.rpcUrl,
+ networkConnectionBannerState.status,
+ networkConnectionBannerState.chainId,
+ );
+ }
+ }, [networkConnectionBannerState, updateRpc]);
+
+ if (!networkConnectionBannerState.visible) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
export default NetworkConnectionBanner;
diff --git a/app/components/UI/Notification/SimpleNotification/index.js b/app/components/UI/Notification/SimpleNotification/index.js
index 8d84853a5ae..79c09153313 100644
--- a/app/components/UI/Notification/SimpleNotification/index.js
+++ b/app/components/UI/Notification/SimpleNotification/index.js
@@ -1,62 +1,32 @@
import React from 'react';
-import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
-import Animated from 'react-native-reanimated';
import BaseNotification from '../../../../component-library/components-temp/BaseNotification';
-import Device from '../../../../util/device';
-import ElevatedView from 'react-native-elevated-view';
-import { colors as importedColors } from '../../../../styles/common';
-
-const styles = StyleSheet.create({
- modalTypeViewBrowser: {
- bottom: Device.isIphoneX() ? 70 : 60,
- },
- elevatedView: {
- backgroundColor: importedColors.transparent,
- },
- notificationContainer: {
- position: 'absolute',
- bottom: 0,
- paddingBottom: Device.isIphoneX() ? 20 : 10,
- left: 0,
- right: 0,
- backgroundColor: importedColors.transparent,
- },
-});
function SimpleNotification({
- isInBrowserView,
- notificationAnimated,
hideCurrentNotification,
+ onDismissComplete,
+ dismissDuration,
currentNotification,
}) {
return (
-
-
-
-
-
+
);
}
SimpleNotification.propTypes = {
- isInBrowserView: PropTypes.bool,
- notificationAnimated: PropTypes.object,
currentNotification: PropTypes.object,
hideCurrentNotification: PropTypes.func,
+ onDismissComplete: PropTypes.func,
+ dismissDuration: PropTypes.number,
};
export default SimpleNotification;
diff --git a/app/components/UI/Notification/TransactionNotification/index.js b/app/components/UI/Notification/TransactionNotification/index.js
index c89bd543254..3a2e62a0779 100644
--- a/app/components/UI/Notification/TransactionNotification/index.js
+++ b/app/components/UI/Notification/TransactionNotification/index.js
@@ -2,23 +2,18 @@ import React, { useEffect, useState, useCallback } from 'react';
import { StyleSheet, View, Text, Dimensions } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import Animated, { useSharedValue } from 'react-native-reanimated';
+import { useSharedValue } from 'react-native-reanimated';
import { strings } from '../../../../../locales/i18n';
import Engine from '../../../../core/Engine';
import { renderFromWei, fastSplit } from '../../../../util/number';
import { validateTransactionActionBalance } from '../../../../util/transactions';
-import {
- fontStyles,
- colors as importedColors,
-} from '../../../../styles/common';
+import { fontStyles } from '../../../../styles/common';
import decodeTransaction from '../../TransactionElement/utils';
import TransactionActionContent from '../../TransactionActionModal/TransactionActionContent';
import ActionContent from '../../ActionModal/ActionContent';
import Ionicons from 'react-native-vector-icons/Ionicons';
import TransactionDetails from '../../TransactionElement/TransactionDetails';
import BaseNotification from '../../../../component-library/components-temp/BaseNotification';
-import Device from '../../../../util/device';
-import ElevatedView from 'react-native-elevated-view';
import { CANCEL_RATE, SPEED_UP_RATE } from '@metamask/transaction-controller';
import BigNumber from 'bignumber.js';
import { collectibleContractsSelector } from '../../../../reducers/collectibles';
@@ -60,17 +55,6 @@ const createStyles = (colors) =>
color: colors.text.default,
...fontStyles.bold,
},
- notification: {
- position: 'absolute',
- bottom: 0,
- paddingBottom: Device.isIphoneX() ? 20 : 10,
- left: 0,
- right: 0,
- backgroundColor: importedColors.transparent,
- },
- modalTypeViewBrowser: {
- bottom: Device.isIphoneX() ? 70 : 60,
- },
closeIcon: {
paddingTop: 4,
position: 'absolute',
@@ -96,18 +80,15 @@ const createStyles = (colors) =>
borderRadius: 10,
backgroundColor: colors.background.default,
},
- elevatedView: {
- backgroundColor: importedColors.transparent,
- },
});
function TransactionNotification(props) {
const {
accounts,
currentNotification,
- isInBrowserView,
- notificationAnimated,
onClose,
+ onDismissComplete,
+ dismissDuration,
transactions,
animatedTimingStart,
smartTransactions,
@@ -286,28 +267,18 @@ function TransactionNotification(props) {
return (
<>
-
-
-
-
-
+
{transactionDetailsIsVisible && (
@@ -369,9 +340,9 @@ function TransactionNotification(props) {
}
TransactionNotification.propTypes = {
- isInBrowserView: PropTypes.bool,
- notificationAnimated: PropTypes.object,
onClose: PropTypes.func,
+ onDismissComplete: PropTypes.func,
+ dismissDuration: PropTypes.number,
animatedTimingStart: PropTypes.func,
currentNotification: PropTypes.object,
swapsTransactions: PropTypes.object,
diff --git a/app/components/UI/Notification/index.js b/app/components/UI/Notification/index.js
index 77f1930faaa..1b00a75f81b 100644
--- a/app/components/UI/Notification/index.js
+++ b/app/components/UI/Notification/index.js
@@ -1,7 +1,6 @@
-import React, { useEffect, useMemo, useCallback } from 'react';
+import React, { useEffect, useCallback } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import { useNavigationState } from '@react-navigation/native';
import {
removeCurrentNotification,
hideCurrentNotification,
@@ -11,28 +10,17 @@ import TransactionNotification from './TransactionNotification';
import SimpleNotification from './SimpleNotification';
import { currentNotificationSelector } from '../../../reducers/notification';
-import { findRouteNameFromNavigatorState } from '../../../util/general';
import usePrevious from '../../hooks/usePrevious';
-import {
- useSharedValue,
- withTiming,
- Easing,
- runOnJS,
-} from 'react-native-reanimated';
+import { withTiming, Easing, runOnJS } from 'react-native-reanimated';
const { TRANSACTION, SIMPLE } = NotificationTypes;
-const BROWSER_ROUTE = 'BrowserView';
-
function Notification({
currentNotification,
currentNotificationIsVisible,
hideCurrentNotification,
removeCurrentNotification,
}) {
- const notificationAnimated = useSharedValue(200);
- const routes = useNavigationState((state) => state.routes);
-
const prevNotificationIsVisible = usePrevious(currentNotificationIsVisible);
const animatedTimingStart = useCallback((animatedRef, toValue, callback) => {
@@ -43,44 +31,22 @@ function Notification({
);
}, []);
- const isInBrowserView = useMemo(
- () => findRouteNameFromNavigatorState(routes) === BROWSER_ROUTE,
- [routes],
- );
-
useEffect(
() => () => {
- animatedTimingStart(notificationAnimated, 200, removeCurrentNotification);
hideCurrentNotification();
+ removeCurrentNotification();
},
- [
- notificationAnimated,
- animatedTimingStart,
- hideCurrentNotification,
- removeCurrentNotification,
- ],
+ [hideCurrentNotification, removeCurrentNotification],
);
useEffect(() => {
if (!prevNotificationIsVisible && currentNotificationIsVisible) {
- animatedTimingStart(notificationAnimated, 0);
hideCurrentNotification();
- setTimeout(() => {
- animatedTimingStart(
- notificationAnimated,
- 200,
- removeCurrentNotification,
- );
- }, currentNotification.autodismiss || 5000);
}
}, [
- animatedTimingStart,
hideCurrentNotification,
- removeCurrentNotification,
currentNotificationIsVisible,
prevNotificationIsVisible,
- currentNotification.autodismiss,
- notificationAnimated,
]);
if (!currentNotification?.type) return null;
@@ -88,8 +54,8 @@ function Notification({
return (
@@ -97,8 +63,9 @@ function Notification({
if (currentNotification.type === SIMPLE)
return (
);
diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.stories.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.stories.tsx
new file mode 100644
index 00000000000..5227b00ebca
--- /dev/null
+++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.stories.tsx
@@ -0,0 +1,113 @@
+import React, { useCallback, useEffect } from 'react';
+import { Alert, ScrollView, View } from 'react-native';
+import { WebSocketConnectionState } from '@metamask/perps-controller';
+import { Text, TextVariant } from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import Button, {
+ ButtonVariants,
+} from '../../../../../component-library/components/Buttons/Button';
+import PerpsWebSocketHealthToast from './PerpsWebSocketHealthToast';
+import {
+ WebSocketHealthToastProvider,
+ useWebSocketHealthToastContext,
+} from './PerpsWebSocketHealthToast.context';
+
+interface WebSocketHealthToastTrigger {
+ label: string;
+ connectionState: WebSocketConnectionState;
+ reconnectionAttempt?: number;
+}
+
+const WEBSOCKET_HEALTH_TOAST_TRIGGERS: WebSocketHealthToastTrigger[] = [
+ {
+ label: 'Disconnected',
+ connectionState: WebSocketConnectionState.Disconnected,
+ },
+ {
+ label: 'Connecting (attempt 1)',
+ connectionState: WebSocketConnectionState.Connecting,
+ reconnectionAttempt: 1,
+ },
+ {
+ label: 'Connecting (attempt 3)',
+ connectionState: WebSocketConnectionState.Connecting,
+ reconnectionAttempt: 3,
+ },
+ {
+ label: 'Connected',
+ connectionState: WebSocketConnectionState.Connected,
+ },
+];
+
+const StoryContainer = ({ children }: { children: React.ReactNode }) => {
+ const tw = useTailwind();
+
+ return (
+
+ {children}
+
+ );
+};
+
+const PerpsWebSocketHealthToastStoryContent = () => {
+ const tw = useTailwind();
+ const { show, hide, setOnRetry } = useWebSocketHealthToastContext();
+
+ useEffect(() => {
+ setOnRetry(() => {
+ Alert.alert('Storybook', 'Retry pressed');
+ });
+ }, [setOnRetry]);
+
+ const triggerToast = useCallback(
+ (trigger: WebSocketHealthToastTrigger) => {
+ show(trigger.connectionState, trigger.reconnectionAttempt ?? 0);
+ },
+ [show],
+ );
+
+ return (
+
+
+
+ Connection states
+ {WEBSOCKET_HEALTH_TOAST_TRIGGERS.map((trigger) => (
+ triggerToast(trigger)}
+ />
+ ))}
+ hide()}
+ />
+
+
+
+
+ );
+};
+
+const PerpsWebSocketHealthToastStory = () => (
+
+
+
+);
+
+const PerpsWebSocketHealthToastMeta = {
+ title: 'Components / UI / Perps / PerpsWebSocketHealthToast',
+ component: PerpsWebSocketHealthToast,
+};
+
+export default PerpsWebSocketHealthToastMeta;
+
+export const Default = {
+ render: () => ,
+};
diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts
index 0090df187eb..d42ed8bf0cd 100644
--- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts
+++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts
@@ -1,8 +1,8 @@
import { StyleSheet } from 'react-native';
-import type { Theme } from '../../../../../util/theme/models';
+import { AppThemeKey, type Theme } from '../../../../../util/theme/models';
const styleSheet = (params: { theme: Theme }) => {
- const { colors } = params.theme;
+ const { colors, shadows } = params.theme;
return StyleSheet.create({
// Toast container - positioned at top of screen
@@ -13,45 +13,39 @@ const styleSheet = (params: { theme: Theme }) => {
right: 12,
zIndex: 9999,
},
- // Wrapper with default background (close wrap: same edges, radius)
- toastWrapper: {
- borderRadius: 12,
- backgroundColor: colors.background.default,
- padding: 2,
- overflow: 'hidden',
- // Shadow for elevation
- shadowColor: colors.shadow.default,
- shadowOffset: {
- width: 0,
- height: 4,
- },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- elevation: 8,
- },
- // Inner toast content (muted background)
toast: {
flexDirection: 'row',
- alignItems: 'center',
- gap: 12,
- paddingVertical: 12,
- paddingHorizontal: 16,
- borderRadius: 10,
- backgroundColor: colors.background.muted,
+ alignItems: 'flex-start',
+ gap: 16,
+ paddingTop: 12,
+ paddingBottom: 12,
+ paddingLeft: 16,
+ paddingRight: 16,
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: colors.border.muted,
+ backgroundColor:
+ params.theme.themeAppearance === AppThemeKey.light
+ ? colors.background.default
+ : colors.background.section,
+ ...(params.theme.themeAppearance === AppThemeKey.light
+ ? shadows.size.md
+ : {}),
},
- // Icon container
iconContainer: {
width: 32,
height: 32,
alignItems: 'center',
justifyContent: 'center',
+ alignSelf: 'flex-start',
},
- // Text content container (title + description)
textContainer: {
flex: 1,
- gap: 2,
+ justifyContent: 'flex-start',
+ },
+ description: {
+ marginTop: 2,
},
- // Retry button
retryButton: {
paddingVertical: 6,
paddingHorizontal: 16,
diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx
index 46eeffbb180..137fe8029b7 100644
--- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx
+++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx
@@ -16,6 +16,7 @@ jest.mock('../../../../../component-library/hooks', () => ({
toast: {},
iconContainer: {},
textContainer: {},
+ description: {},
retryButton: {},
},
}),
diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx
index 4005e869ee8..081940ab83f 100644
--- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx
+++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx
@@ -243,48 +243,50 @@ const PerpsWebSocketHealthToast: React.FC = memo(() => {
pointerEvents="box-none"
{...panResponder.panHandlers}
>
-
-
- {/* Icon or Spinner */}
-
- {toastConfig.showSpinner ? (
-
- ) : (
-
- )}
-
-
- {/* Text Content */}
-
-
- {toastConfig.title}
-
-
- {toastConfig.description}
-
-
+
+ {/* Icon or Spinner */}
+
+ {toastConfig.showSpinner ? (
+
+ ) : (
+
+ )}
+
- {/* Retry Button - only shown when disconnected */}
- {connectionState === WebSocketConnectionState.Disconnected &&
- onRetry && (
-
-
- {strings('perps.connection.websocket_retry')}
-
-
- )}
+ {/* Text Content */}
+
+
+ {toastConfig.title}
+
+
+ {toastConfig.description}
+
+
+ {/* Retry Button - only shown when disconnected */}
+ {connectionState === WebSocketConnectionState.Disconnected &&
+ onRetry && (
+
+
+ {strings('perps.connection.websocket_retry')}
+
+
+ )}
diff --git a/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx b/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
index 9ff77c33e48..beb057b6c85 100644
--- a/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
@@ -7,7 +7,10 @@ import {
ButtonIconVariant,
ToastVariants,
} from '../../../../component-library/components/Toast/Toast.types';
-import { IconName } from '../../../../component-library/components/Icons/Icon';
+import {
+ IconColor,
+ IconName,
+} from '../../../../component-library/components/Icons/Icon';
import { ButtonVariants } from '../../../../component-library/components/Buttons/Button';
import Routes from '../../../../constants/navigation/Routes';
import { mockTheme } from '../../../../util/theme';
@@ -34,20 +37,26 @@ jest.mock('../../../../util/theme', () => {
jest.mock('@metamask/design-system-react-native', () => ({
IconSize: {
+ Lg: 'lg',
Xl: 'xl',
},
IconColor: {
+ IconDefault: 'icon-default',
PrimaryDefault: 'primary-default',
},
Text: 'Text',
TextVariant: {
BodyMd: 'BodyMd',
+ BodySm: 'BodySm',
BodySMMedium: 'BodySMMedium',
},
TextColor: {
TextDefault: 'TextDefault',
},
Spinner: 'Spinner',
+ FontWeight: {
+ Medium: 'medium',
+ },
}));
jest.mock('../utils/translatePerpsError', () => ({
@@ -97,7 +106,7 @@ describe('usePerpsToasts', () => {
const { result } = renderHook(() => usePerpsToasts());
const testConfig = {
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
labelOptions: [{ label: 'Test', isBold: true }],
hasNoTimeout: false,
@@ -109,7 +118,7 @@ describe('usePerpsToasts', () => {
expect(mockShowToast).toHaveBeenCalledWith({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
labelOptions: [{ label: 'Test', isBold: true }],
hasNoTimeout: false,
});
@@ -132,8 +141,7 @@ describe('usePerpsToasts', () => {
expect.objectContaining({
variant: ToastVariants.Icon,
iconName: IconName.Error,
- iconColor: mockTheme.colors.error.default,
- backgroundColor: mockTheme.colors.accent04.normal,
+ iconColor: IconColor.Error,
linkButtonOptions: {
label: 'Try again',
onPress: onRetry,
@@ -155,7 +163,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
hasNoTimeout: false,
});
@@ -293,8 +301,7 @@ describe('usePerpsToasts', () => {
});
expect(config).toMatchObject({
iconName: IconName.Error,
- iconColor: mockTheme.colors.error.default,
- backgroundColor: mockTheme.colors.accent04.normal,
+ iconColor: IconColor.Error,
});
});
});
@@ -332,7 +339,7 @@ describe('usePerpsToasts', () => {
);
expect(config).toMatchObject({
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
});
expect(config.labelOptions).toContainEqual({
label: 'Order filled',
@@ -432,7 +439,7 @@ describe('usePerpsToasts', () => {
);
expect(config).toMatchObject({
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
});
expect(config.labelOptions).toContainEqual({
label: 'Order placed',
@@ -589,7 +596,7 @@ describe('usePerpsToasts', () => {
]);
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
});
});
@@ -611,7 +618,7 @@ describe('usePerpsToasts', () => {
});
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
});
});
@@ -635,7 +642,7 @@ describe('usePerpsToasts', () => {
});
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
});
});
@@ -746,7 +753,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
});
expect(config.labelOptions).toHaveLength(3);
@@ -820,7 +827,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
});
expect(config.labelOptions).toHaveLength(3);
@@ -867,7 +874,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
});
expect(config.labelOptions).toContainEqual({
@@ -892,7 +899,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
hasNoTimeout: false,
});
@@ -912,7 +919,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
hasNoTimeout: false,
});
@@ -977,7 +984,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
hasNoTimeout: false,
});
@@ -1108,7 +1115,7 @@ describe('usePerpsToasts', () => {
expect(config).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
hapticsType: NotificationMoment.Success,
hasNoTimeout: false,
});
@@ -1151,7 +1158,7 @@ describe('usePerpsToasts', () => {
// Check that the configs use the correct variants and icons
expect(successConfig).toMatchObject({
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
+ iconName: IconName.Confirmation,
});
expect(errorConfig).toMatchObject({
variant: ToastVariants.Icon,
diff --git a/app/components/UI/Perps/hooks/usePerpsToasts.tsx b/app/components/UI/Perps/hooks/usePerpsToasts.tsx
index d5f79e08a14..2b122fee554 100644
--- a/app/components/UI/Perps/hooks/usePerpsToasts.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsToasts.tsx
@@ -1,9 +1,9 @@
import {
- IconColor as ReactNativeDsIconColor,
IconSize as ReactNativeDsIconSize,
Text,
TextVariant,
Spinner,
+ FontWeight,
} from '@metamask/design-system-react-native';
import { useNavigation } from '@react-navigation/native';
import {
@@ -12,10 +12,12 @@ import {
type HapticNotificationMoment,
} from '../../../../util/haptics';
import React, { useCallback, useContext, useMemo } from 'react';
-import { StyleSheet, View } from 'react-native';
import { strings } from '../../../../../locales/i18n';
import { ButtonVariants } from '../../../../component-library/components/Buttons/Button';
-import { IconName } from '../../../../component-library/components/Icons/Icon';
+import {
+ IconColor,
+ IconName,
+} from '../../../../component-library/components/Icons/Icon';
import { ToastContext } from '../../../../component-library/components/Toast';
import {
ButtonIconVariant,
@@ -232,15 +234,6 @@ const PERPS_TOASTS_DEFAULT_OPTIONS: Partial = {
hasNoTimeout: false,
};
-const toastStyles = StyleSheet.create({
- spinnerContainer: {
- paddingRight: 12,
- alignContent: 'center',
- alignItems: 'center',
- justifyContent: 'center',
- },
-});
-
const usePerpsToasts = (): {
showToast: (config: PerpsToastOptions) => void;
PerpsToastOptions: PerpsToastOptionsConfig;
@@ -254,9 +247,8 @@ const usePerpsToasts = (): {
success: {
...(PERPS_TOASTS_DEFAULT_OPTIONS as PerpsToastOptions),
variant: ToastVariants.Icon,
- iconName: IconName.CheckBold,
- iconColor: theme.colors.accent03.dark,
- backgroundColor: theme.colors.accent03.normal,
+ iconName: IconName.Confirmation,
+ iconColor: IconColor.Success,
hapticsType: NotificationMoment.Success,
},
// Intentional duplication for now to avoid coupling with success options.
@@ -264,44 +256,34 @@ const usePerpsToasts = (): {
...(PERPS_TOASTS_DEFAULT_OPTIONS as PerpsToastOptions),
variant: ToastVariants.Icon,
iconName: IconName.Loading,
- iconColor: theme.colors.accent04.dark,
- backgroundColor: theme.colors.accent04.normal,
hapticsType: NotificationMoment.Warning,
startAccessory: (
-
-
-
+
),
},
info: {
...(PERPS_TOASTS_DEFAULT_OPTIONS as PerpsToastOptions),
variant: ToastVariants.Icon,
iconName: IconName.Info,
- iconColor: theme.colors.icon.default,
- backgroundColor: theme.colors.background.alternative,
+ iconColor: IconColor.Default,
hapticsType: NotificationMoment.Warning,
},
error: {
...(PERPS_TOASTS_DEFAULT_OPTIONS as PerpsToastOptions),
variant: ToastVariants.Icon,
iconName: IconName.Warning,
- iconColor: theme.colors.accent01.dark,
- backgroundColor: theme.colors.accent01.light,
+ iconColor: IconColor.Error,
hapticsType: NotificationMoment.Error,
},
warning: {
...(PERPS_TOASTS_DEFAULT_OPTIONS as PerpsToastOptions),
variant: ToastVariants.Icon,
iconName: IconName.Warning,
- iconColor: theme.colors.warning.default,
- backgroundColor: theme.colors.warning.muted,
+ iconColor: IconColor.Warning,
hapticsType: NotificationMoment.Warning,
},
}),
- [theme],
+ [],
);
const navigationHandlers = useMemo(
@@ -435,14 +417,14 @@ const usePerpsToasts = (): {
closeButtonOptions: {
label: (
{strings('perps.deposit.cancel_trade')}
),
variant: ButtonVariants.Secondary,
- style: { backgroundColor: theme.colors.background.muted },
onPress: () => {
/* no-op */
},
@@ -452,8 +434,7 @@ const usePerpsToasts = (): {
...(PERPS_TOASTS_DEFAULT_OPTIONS as PerpsToastOptions),
variant: ToastVariants.Icon,
iconName: IconName.Warning,
- iconColor: theme.colors.error.default,
- backgroundColor: theme.colors.error.muted,
+ iconColor: IconColor.Error,
hapticsType: NotificationMoment.Warning,
labelOptions: getPerpsToastLabels(
strings('perps.deposit.trade_canceled'),
@@ -512,8 +493,7 @@ const usePerpsToasts = (): {
withdrawalStartFailed: (onRetry: () => void) => ({
...perpsBaseToastOptions.error,
iconName: IconName.Error,
- iconColor: theme.colors.error.default,
- backgroundColor: theme.colors.accent04.normal,
+ iconColor: IconColor.Error,
labelOptions: getPerpsToastLabels(
strings('perps.withdrawal.toast_error_title'),
strings('perps.withdrawal.toast_start_error_description'),
@@ -1039,10 +1019,7 @@ const usePerpsToasts = (): {
perpsBaseToastOptions.success,
perpsBaseToastOptions.warning,
perpsToastButtonOptions,
- theme.colors.accent04.normal,
- theme.colors.background.muted,
theme.colors.error.default,
- theme.colors.error.muted,
theme.colors.success.default,
toastRef,
],
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.test.tsx b/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.test.tsx
index e084cd8c272..d5286ebf013 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.test.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.test.tsx
@@ -17,7 +17,6 @@ jest.mock('../../../../util/theme', () => ({
colors: {
success: { default: 'successDefault' },
error: { default: 'errorDefault' },
- accent04: { normal: 'accent04Normal' },
},
}),
}));
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.tsx b/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.tsx
index acdb4342c84..0cf2f9f899a 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawToastRegistrations.tsx
@@ -1,5 +1,4 @@
import {
- Box,
IconColor as ReactNativeDsIconColor,
IconSize as ReactNativeDsIconSize,
Spinner,
@@ -78,12 +77,10 @@ export const usePerpsWithdrawToastRegistrations = (): ToastRegistration[] => {
iconName: IconName.Loading,
hasNoTimeout: false,
startAccessory: (
-
-
-
+
),
});
return;
@@ -129,16 +126,11 @@ export const usePerpsWithdrawToastRegistrations = (): ToastRegistration[] => {
],
iconName: IconName.Error,
iconColor: theme.colors.error.default,
- backgroundColor: theme.colors.accent04.normal,
hasNoTimeout: false,
});
}
},
- [
- theme.colors.accent04.normal,
- theme.colors.error.default,
- theme.colors.success.default,
- ],
+ [theme.colors.error.default, theme.colors.success.default],
);
return useMemo(
diff --git a/app/components/UI/Predict/hooks/predictTransactionStatusChangedToastHandler.tsx b/app/components/UI/Predict/hooks/predictTransactionStatusChangedToastHandler.tsx
new file mode 100644
index 00000000000..e8e940d3ee4
--- /dev/null
+++ b/app/components/UI/Predict/hooks/predictTransactionStatusChangedToastHandler.tsx
@@ -0,0 +1,412 @@
+import {
+ IconColor as ReactNativeDsIconColor,
+ IconSize as ReactNativeDsIconSize,
+ Spinner,
+} from '@metamask/design-system-react-native';
+import type { QueryClient } from '@tanstack/react-query';
+import React from 'react';
+import { strings } from '../../../../../locales/i18n';
+import { IconName } from '../../../../component-library/components/Icons/Icon';
+import { ToastVariants } from '../../../../component-library/components/Toast';
+import { ButtonVariants } from '../../../../component-library/components/Buttons/Button';
+import type { ToastRef } from '../../../../component-library/components/Toast/Toast.types';
+import { navigateToTransactionDetails } from '../../../../util/navigation/navigateToTransactionDetails';
+// eslint-disable-next-line import-x/no-restricted-paths -- TODO(ADR-0020): shared activity type-filter; route-isolation backlog
+import { ActivityTypeFilter } from '../../../Views/ActivityScreen/types';
+import type { Theme } from '../../../../util/theme/models';
+import { PredictEventValues } from '../constants/eventNames';
+import type { PredictTransactionStatusChangedPayload } from '../controllers/PredictController';
+import { formatPrice } from '../utils/format';
+import { predictQueries } from '../queries';
+
+const showPendingToast = ({
+ showToast,
+ title,
+ description,
+ trackLabel,
+ onTrack,
+}: {
+ showToast: ToastRef['showToast'];
+ title: string;
+ description: string;
+ trackLabel?: string;
+ onTrack?: () => void;
+}) =>
+ showToast({
+ variant: ToastVariants.Icon,
+ labelOptions: [
+ { label: title, isBold: true },
+ { label: '\n', isBold: false },
+ { label: description, isBold: false },
+ ],
+ iconName: IconName.Loading,
+ hasNoTimeout: false,
+ startAccessory: (
+
+ ),
+ ...(trackLabel && onTrack
+ ? {
+ closeButtonOptions: {
+ label: trackLabel,
+ onPress: onTrack,
+ variant: ButtonVariants.Link,
+ },
+ }
+ : {}),
+ });
+
+const showSuccessToast = ({
+ showToast,
+ title,
+ description,
+ iconColor,
+ iconName = IconName.Confirmation,
+}: {
+ showToast: ToastRef['showToast'];
+ title: string;
+ description: string;
+ iconColor: string;
+ iconName?: IconName;
+}) =>
+ showToast({
+ variant: ToastVariants.Icon,
+ labelOptions: [
+ { label: title, isBold: true },
+ { label: '\n', isBold: false },
+ { label: description, isBold: false },
+ ],
+ iconName,
+ iconColor,
+ hasNoTimeout: false,
+ });
+
+const showErrorToast = ({
+ showToast,
+ title,
+ description,
+ retryLabel,
+ onRetry,
+ iconColor,
+}: {
+ showToast: ToastRef['showToast'];
+ title: string;
+ description: string;
+ retryLabel?: string;
+ onRetry?: () => void;
+ iconColor: string;
+}) =>
+ showToast({
+ variant: ToastVariants.Icon,
+ labelOptions: [
+ { label: title, isBold: true },
+ { label: '\n', isBold: false },
+ { label: description, isBold: false },
+ ],
+ iconName: IconName.Error,
+ iconColor,
+ hasNoTimeout: false,
+ ...(retryLabel && onRetry
+ ? {
+ linkButtonOptions: {
+ label: retryLabel,
+ onPress: onRetry,
+ },
+ }
+ : {}),
+ });
+
+export interface PredictTransactionStatusChangedHandlerDeps {
+ queryClient: QueryClient;
+ navigation: Parameters[0];
+ theme: Pick;
+ normalizedSelectedAddress: string;
+ bottomSheetEnabled: boolean;
+ deposit: () => Promise;
+ claim: (params: {
+ entryPoint: (typeof PredictEventValues.ENTRY_POINT)[keyof typeof PredictEventValues.ENTRY_POINT];
+ }) => Promise;
+ withdraw: () => Promise;
+ withdrawTransactionAmount?: number;
+ getTransactionMetadata: (transactionId: string) => unknown;
+ isPerpsPredictMoneyDeposit: (metadata: unknown) => boolean;
+ isPerpsPredictMoneyWithdraw: (metadata: unknown) => boolean;
+ shouldSuppressLegacyOrderFailureToast: () => boolean;
+ getWithdrawConfirmedMessage: (
+ transactionId: string | undefined,
+ fallbackAmount: number,
+ ) => { title: string; description: string };
+}
+
+export const createPredictTransactionStatusChangedHandler =
+ ({
+ queryClient,
+ navigation,
+ theme,
+ normalizedSelectedAddress,
+ bottomSheetEnabled,
+ deposit,
+ claim,
+ withdraw,
+ withdrawTransactionAmount,
+ getTransactionMetadata,
+ isPerpsPredictMoneyDeposit,
+ isPerpsPredictMoneyWithdraw,
+ shouldSuppressLegacyOrderFailureToast,
+ getWithdrawConfirmedMessage,
+ }: PredictTransactionStatusChangedHandlerDeps) =>
+ (payload: unknown, showToast: ToastRef['showToast']): void => {
+ const { type, status, senderAddress, transactionId, amount } =
+ payload as PredictTransactionStatusChangedPayload;
+ const canRetry =
+ Boolean(senderAddress) && senderAddress === normalizedSelectedAddress;
+
+ if (status === 'confirmed') {
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.balance.keys.all(),
+ });
+
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.unrealizedPnL.keys.all(),
+ });
+
+ if (type === 'claim' || type === 'order') {
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.positions.keys.all(),
+ });
+
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.activity.keys.all(),
+ });
+ }
+ }
+
+ if (type === 'deposit') {
+ const depositMeta = transactionId
+ ? getTransactionMetadata(transactionId)
+ : undefined;
+ if (depositMeta && isPerpsPredictMoneyDeposit(depositMeta)) {
+ return;
+ }
+
+ if (status === 'approved') {
+ showPendingToast({
+ showToast,
+ title: strings('predict.deposit.adding_funds'),
+ description: strings('predict.deposit.available_in_minutes', {
+ minutes: 1,
+ }),
+ trackLabel: strings('predict.deposit.track'),
+ onTrack: () => {
+ navigateToTransactionDetails(navigation, {
+ transactionId,
+ initialTypeFilter: ActivityTypeFilter.Predictions,
+ });
+ },
+ });
+ return;
+ }
+
+ if (status === 'confirmed') {
+ const depositAmount =
+ typeof amount === 'number' && amount > 0
+ ? formatPrice(amount, {
+ maximumDecimals: 2,
+ })
+ : strings('predict.deposit.account_ready');
+
+ showSuccessToast({
+ showToast,
+ title: strings('predict.deposit.ready_to_trade'),
+ description: strings('predict.deposit.account_ready_description', {
+ amount: depositAmount,
+ }),
+ iconColor: theme.colors.success.default,
+ });
+ return;
+ }
+
+ if (status === 'failed') {
+ showErrorToast({
+ showToast,
+ title: strings('predict.deposit.error_title'),
+ description: strings('predict.deposit.error_description'),
+ ...(canRetry
+ ? {
+ retryLabel: strings('predict.deposit.try_again'),
+ onRetry: () => {
+ deposit().catch(() => undefined);
+ },
+ }
+ : {}),
+ iconColor: theme.colors.error.default,
+ });
+ return;
+ }
+
+ return;
+ }
+
+ if (type === 'claim') {
+ const formattedClaimAmount = formatPrice(amount ?? 0, {
+ maximumDecimals: 2,
+ });
+
+ if (status === 'approved') {
+ showPendingToast({
+ showToast,
+ title: strings('predict.claim.toasts.pending.title', {
+ amount: formattedClaimAmount,
+ }),
+ description: strings('predict.claim.toasts.pending.description', {
+ time: 5,
+ }),
+ });
+ return;
+ }
+
+ if (status === 'confirmed') {
+ if ((amount ?? 0) <= 0) {
+ showSuccessToast({
+ showToast,
+ title: strings('predict.claim.toasts.redeemed.title'),
+ description: strings('predict.claim.toasts.redeemed.description'),
+ iconName: IconName.Info,
+ iconColor: theme.colors.primary.default,
+ });
+ return;
+ }
+
+ showSuccessToast({
+ showToast,
+ title: strings('predict.claim.toasts.confirmed.title'),
+ description: strings('predict.claim.toasts.confirmed.description', {
+ amount: formattedClaimAmount,
+ }),
+ iconColor: theme.colors.success.default,
+ });
+ return;
+ }
+
+ if (status === 'failed') {
+ showErrorToast({
+ showToast,
+ title: strings('predict.claim.toasts.error.title'),
+ description: strings('predict.claim.toasts.error.description'),
+ ...(canRetry
+ ? {
+ retryLabel: strings('predict.claim.toasts.error.try_again'),
+ onRetry: () => {
+ claim({
+ entryPoint: PredictEventValues.ENTRY_POINT.BACKGROUND,
+ }).catch(() => undefined);
+ },
+ }
+ : {}),
+ iconColor: theme.colors.error.default,
+ });
+ }
+
+ return;
+ }
+
+ if (type === 'withdraw') {
+ if (status === 'approved') {
+ showPendingToast({
+ showToast,
+ title: strings('predict.withdraw.withdrawing'),
+ description: strings('predict.withdraw.withdrawing_subtitle'),
+ });
+ return;
+ }
+
+ if (status === 'confirmed') {
+ const withdrawMeta = transactionId
+ ? getTransactionMetadata(transactionId)
+ : undefined;
+ if (withdrawMeta && isPerpsPredictMoneyWithdraw(withdrawMeta)) {
+ return;
+ }
+
+ const fallbackAmount = amount ?? withdrawTransactionAmount ?? 0;
+ const { title, description } = getWithdrawConfirmedMessage(
+ transactionId,
+ fallbackAmount,
+ );
+
+ showSuccessToast({
+ showToast,
+ title,
+ description,
+ iconColor: theme.colors.success.default,
+ });
+ return;
+ }
+
+ if (status === 'failed') {
+ showErrorToast({
+ showToast,
+ title: strings('predict.withdraw.error_title'),
+ description: strings('predict.withdraw.error_description'),
+ ...(canRetry
+ ? {
+ retryLabel: strings('predict.withdraw.try_again'),
+ onRetry: () => {
+ withdraw().catch(() => undefined);
+ },
+ }
+ : {}),
+ iconColor: theme.colors.error.default,
+ });
+ return;
+ }
+ }
+
+ if (type === 'order') {
+ if (status === 'depositing') {
+ queryClient.invalidateQueries({
+ queryKey: predictQueries.positions.keys.all(),
+ });
+
+ showPendingToast({
+ showToast,
+ title: strings('predict.order.prediction_in_progress'),
+ description: strings(
+ 'predict.order.prediction_in_progress_description',
+ ),
+ });
+ return;
+ }
+
+ if (status === 'confirmed') {
+ showToast({
+ variant: ToastVariants.Icon,
+ iconName: IconName.Confirmation,
+ iconColor: theme.colors.success.default,
+ labelOptions: [
+ {
+ label: strings('predict.order.prediction_placed'),
+ isBold: true,
+ },
+ ],
+ hasNoTimeout: false,
+ });
+ return;
+ }
+
+ if (status === 'failed') {
+ if (bottomSheetEnabled && shouldSuppressLegacyOrderFailureToast()) {
+ return;
+ }
+ showErrorToast({
+ showToast,
+ title: strings('predict.order.prediction_failed'),
+ description: strings('predict.order.order_failed_generic'),
+ iconColor: theme.colors.error.default,
+ });
+ return;
+ }
+ }
+ };
diff --git a/app/components/UI/Predict/hooks/usePredictShare.utils.tsx b/app/components/UI/Predict/hooks/usePredictShare.utils.tsx
index a5ab944fa49..de8cc7486a1 100644
--- a/app/components/UI/Predict/hooks/usePredictShare.utils.tsx
+++ b/app/components/UI/Predict/hooks/usePredictShare.utils.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import { Box } from '@metamask/design-system-react-native';
import Icon, {
IconName,
IconSize,
@@ -26,14 +25,11 @@ export const buildShareCopiedToastOptions = ({
backgroundColor: 'transparent',
iconColor: successColor,
hasNoTimeout: false,
- customBottomOffset: -50,
startAccessory: (
-
-
-
+
),
});
diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx
index 6f9164f608d..ec783175dfe 100644
--- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx
+++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx
@@ -988,7 +988,7 @@ describe('usePredictToastRegistrations', () => {
expect(showToast).toHaveBeenCalledWith(
expect.objectContaining({
variant: 'Icon',
- iconName: 'Check',
+ iconName: 'Confirmation',
hasNoTimeout: false,
}),
);
diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
index df3eb6640e1..cb33427da98 100644
--- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
+++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
@@ -1,28 +1,11 @@
-import {
- Box,
- IconColor as ReactNativeDsIconColor,
- IconSize as ReactNativeDsIconSize,
- Spinner,
-} from '@metamask/design-system-react-native';
import { useNavigation } from '@react-navigation/native';
import { useQueryClient } from '@tanstack/react-query';
-import React, { useCallback, useMemo } from 'react';
+import { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
-import { strings } from '../../../../../locales/i18n';
-import { IconName } from '../../../../component-library/components/Icons/Icon';
-import { ToastVariants } from '../../../../component-library/components/Toast';
-import { ButtonVariants } from '../../../../component-library/components/Buttons/Button';
import type { ToastRef } from '../../../../component-library/components/Toast/Toast.types';
-import { navigateToTransactionDetails } from '../../../../util/navigation/navigateToTransactionDetails';
-// eslint-disable-next-line import-x/no-restricted-paths -- TODO(ADR-0020): shared activity type-filter; route-isolation backlog
-import { ActivityTypeFilter } from '../../../Views/ActivityScreen/types';
import type { ToastRegistration } from '../../../Nav/App/ControllerEventToastBridge';
import { useAppThemeFromContext } from '../../../../util/theme';
-import { PredictEventValues } from '../constants/eventNames';
-import type { PredictTransactionStatusChangedPayload } from '../controllers/PredictController';
import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts';
-import { formatPrice } from '../utils/format';
-import { predictQueries } from '../queries';
import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController';
import { usePredictClaim } from './usePredictClaim';
import { usePredictDeposit } from './usePredictDeposit';
@@ -36,110 +19,40 @@ import {
import { resolveWithdrawTokenInfo } from '../../../Views/confirmations/utils/withdraw-token-resolution';
import { selectPredictBottomSheetEnabledFlag } from '../selectors/featureFlags';
import { shouldSuppressLegacyOrderFailureToast } from '../contexts/PredictPreviewSheetContext';
+import { strings } from '../../../../../locales/i18n';
+import { formatPrice } from '../utils/format';
+import { createPredictTransactionStatusChangedHandler } from './predictTransactionStatusChangedToastHandler';
-const showPendingToast = ({
- showToast,
- title,
- description,
- trackLabel,
- onTrack,
-}: {
- showToast: ToastRef['showToast'];
- title: string;
- description: string;
- trackLabel?: string;
- onTrack?: () => void;
-}) =>
- showToast({
- variant: ToastVariants.Icon,
- labelOptions: [
- { label: title, isBold: true },
- { label: '\n', isBold: false },
- { label: description, isBold: false },
- ],
- iconName: IconName.Loading,
- hasNoTimeout: false,
- startAccessory: (
-
-
-
- ),
- ...(trackLabel && onTrack
- ? {
- closeButtonOptions: {
- label: trackLabel,
- onPress: onTrack,
- variant: ButtonVariants.Link,
- },
- }
- : {}),
- });
+function getWithdrawConfirmedMessage(
+ transactionId: string | undefined,
+ fallbackAmount: number,
+): { title: string; description: string } {
+ const title = strings('predict.withdraw.withdraw_completed');
-const showSuccessToast = ({
- showToast,
- title,
- description,
- iconColor,
- iconName = IconName.Confirmation,
-}: {
- showToast: ToastRef['showToast'];
- title: string;
- description: string;
- iconColor: string;
- iconName?: IconName;
-}) =>
- showToast({
- variant: ToastVariants.Icon,
- labelOptions: [
- { label: title, isBold: true },
- { label: '\n', isBold: false },
- { label: description, isBold: false },
- ],
- iconName,
- iconColor,
- hasNoTimeout: false,
- });
+ const { isPostQuote, targetFiat, tokenSymbol } = resolveWithdrawTokenInfo(
+ store.getState(),
+ transactionId,
+ );
-const showErrorToast = ({
- showToast,
- title,
- description,
- retryLabel,
- onRetry,
- backgroundColor,
- iconColor,
-}: {
- showToast: ToastRef['showToast'];
- title: string;
- description: string;
- retryLabel?: string;
- onRetry?: () => void;
- backgroundColor: string;
- iconColor: string;
-}) =>
- showToast({
- variant: ToastVariants.Icon,
- labelOptions: [
- { label: title, isBold: true },
- { label: '\n', isBold: false },
- { label: description, isBold: false },
- ],
- iconName: IconName.Error,
- iconColor,
- backgroundColor,
- hasNoTimeout: false,
- ...(retryLabel && onRetry
- ? {
- linkButtonOptions: {
- label: retryLabel,
- onPress: onRetry,
- },
- }
- : {}),
- });
+ if (!isPostQuote) {
+ return {
+ title,
+ description: strings('predict.withdraw.withdraw_completed_subtitle', {
+ amount: formatPrice(fallbackAmount),
+ }),
+ };
+ }
+
+ const withdrawAmount = targetFiat ?? fallbackAmount;
+
+ return {
+ title,
+ description: strings(
+ 'predict.withdraw.withdraw_any_token_completed_subtitle',
+ { amount: formatPrice(withdrawAmount), token: tokenSymbol },
+ ),
+ };
+}
export const usePredictToastRegistrations = (): ToastRegistration[] => {
const queryClient = useQueryClient();
@@ -149,273 +62,30 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
const navigation = useNavigation();
const theme = useAppThemeFromContext();
- // Subscribe to account group changes so the hook re-renders when the user switches accounts
useSelector(selectSelectedAccountGroupId);
const bottomSheetEnabled = useSelector(selectPredictBottomSheetEnabledFlag);
const selectedAddress = getEvmAccountFromSelectedAccountGroup()?.address;
const normalizedSelectedAddress = selectedAddress?.toLowerCase() ?? '';
+
const handleTransactionStatusChanged = useCallback(
(payload: unknown, showToast: ToastRef['showToast']): void => {
- const { type, status, senderAddress, transactionId, amount, marketId } =
- payload as PredictTransactionStatusChangedPayload;
- const canRetry =
- Boolean(senderAddress) && senderAddress === normalizedSelectedAddress;
-
- if (status === 'confirmed') {
- queryClient.invalidateQueries({
- queryKey: predictQueries.balance.keys.all(),
- });
-
- queryClient.invalidateQueries({
- queryKey: predictQueries.unrealizedPnL.keys.all(),
- });
-
- // Deposit/Withdraw should not invalidate positions/activity
- if (type === 'claim' || type === 'order') {
- queryClient.invalidateQueries({
- queryKey: predictQueries.positions.keys.all(),
- });
-
- queryClient.invalidateQueries({
- queryKey: predictQueries.activity.keys.all(),
- });
- }
- }
-
- if (type === 'deposit') {
- const depositMeta = transactionId
- ? selectTransactionMetadataById(store.getState(), transactionId)
- : undefined;
- if (depositMeta && isPerpsPredictMoneyDeposit(depositMeta)) {
- return;
- }
-
- if (status === 'approved') {
- showPendingToast({
- showToast,
- title: strings('predict.deposit.adding_funds'),
- description: strings('predict.deposit.available_in_minutes', {
- minutes: 1,
- }),
- trackLabel: strings('predict.deposit.track'),
- onTrack: () => {
- navigateToTransactionDetails(navigation, {
- transactionId,
- initialTypeFilter: ActivityTypeFilter.Predictions,
- });
- },
- });
- return;
- }
-
- if (status === 'confirmed') {
- const depositAmount =
- typeof amount === 'number' && amount > 0
- ? formatPrice(amount, {
- maximumDecimals: 2,
- })
- : strings('predict.deposit.account_ready');
-
- showSuccessToast({
- showToast,
- title: strings('predict.deposit.ready_to_trade'),
- description: strings('predict.deposit.account_ready_description', {
- amount: depositAmount,
- }),
- iconColor: theme.colors.success.default,
- });
- return;
- }
-
- if (status === 'failed') {
- showErrorToast({
- showToast,
- title: strings('predict.deposit.error_title'),
- description: strings('predict.deposit.error_description'),
- ...(canRetry
- ? {
- retryLabel: strings('predict.deposit.try_again'),
- onRetry: () => {
- deposit().catch(() => undefined);
- },
- }
- : {}),
- backgroundColor: theme.colors.accent04.normal,
- iconColor: theme.colors.error.default,
- });
- return;
- }
-
- return;
- }
-
- if (type === 'claim') {
- const formattedClaimAmount = formatPrice(amount ?? 0, {
- maximumDecimals: 2,
- });
-
- if (status === 'approved') {
- showPendingToast({
- showToast,
- title: strings('predict.claim.toasts.pending.title', {
- amount: formattedClaimAmount,
- }),
- description: strings('predict.claim.toasts.pending.description', {
- time: 5,
- }),
- });
- return;
- }
-
- if (status === 'confirmed') {
- if ((amount ?? 0) <= 0) {
- showSuccessToast({
- showToast,
- title: strings('predict.claim.toasts.redeemed.title'),
- description: strings('predict.claim.toasts.redeemed.description'),
- iconName: IconName.Info,
- iconColor: theme.colors.primary.default,
- });
- return;
- }
-
- showSuccessToast({
- showToast,
- title: strings('predict.claim.toasts.confirmed.title'),
- description: strings('predict.claim.toasts.confirmed.description', {
- amount: formattedClaimAmount,
- }),
- iconColor: theme.colors.success.default,
- });
- return;
- }
-
- if (status === 'failed') {
- showErrorToast({
- showToast,
- title: strings('predict.claim.toasts.error.title'),
- description: strings('predict.claim.toasts.error.description'),
- ...(canRetry
- ? {
- retryLabel: strings('predict.claim.toasts.error.try_again'),
- onRetry: () => {
- claim({
- entryPoint: PredictEventValues.ENTRY_POINT.BACKGROUND,
- }).catch(() => undefined);
- },
- }
- : {}),
- backgroundColor: theme.colors.accent04.normal,
- iconColor: theme.colors.error.default,
- });
- }
-
- return;
- }
-
- if (type === 'withdraw') {
- if (status === 'approved') {
- showPendingToast({
- showToast,
- title: strings('predict.withdraw.withdrawing'),
- description: strings('predict.withdraw.withdrawing_subtitle'),
- });
- return;
- }
-
- if (status === 'confirmed') {
- const withdrawMeta = transactionId
- ? selectTransactionMetadataById(store.getState(), transactionId)
- : undefined;
- if (withdrawMeta && isPerpsPredictMoneyWithdraw(withdrawMeta)) {
- return;
- }
-
- const fallbackAmount = amount ?? withdrawTransaction?.amount ?? 0;
- const { title, description } = getWithdrawConfirmedMessage(
- transactionId,
- fallbackAmount,
- );
-
- showSuccessToast({
- showToast,
- title,
- description,
- iconColor: theme.colors.success.default,
- });
- return;
- }
-
- if (status === 'failed') {
- showErrorToast({
- showToast,
- title: strings('predict.withdraw.error_title'),
- description: strings('predict.withdraw.error_description'),
- ...(canRetry
- ? {
- retryLabel: strings('predict.withdraw.try_again'),
- onRetry: () => {
- withdraw().catch(() => undefined);
- },
- }
- : {}),
- backgroundColor: theme.colors.accent04.normal,
- iconColor: theme.colors.error.default,
- });
- return;
- }
- }
-
- if (type === 'order') {
- if (status === 'depositing') {
- queryClient.invalidateQueries({
- queryKey: predictQueries.positions.keys.all(),
- });
-
- showPendingToast({
- showToast,
- title: strings('predict.order.prediction_in_progress'),
- description: strings(
- 'predict.order.prediction_in_progress_description',
- ),
- });
- return;
- }
-
- if (status === 'confirmed') {
- showToast({
- variant: ToastVariants.Icon,
- iconName: IconName.Check,
- iconColor: theme.colors.success.default,
- labelOptions: [
- {
- label: strings('predict.order.prediction_placed'),
- isBold: true,
- },
- ],
- hasNoTimeout: false,
- });
- return;
- }
-
- if (status === 'failed') {
- // When the bottom-sheet flow is on and the provider is mounted,
- // the provider's state-based trigger (in PredictPreviewSheetContext)
- // surfaces a persistent Retry toast for the same error. Skip here
- // to avoid double-firing.
- if (bottomSheetEnabled && shouldSuppressLegacyOrderFailureToast()) {
- return;
- }
- showErrorToast({
- showToast,
- title: strings('predict.order.prediction_failed'),
- description: strings('predict.order.order_failed_generic'),
- backgroundColor: theme.colors.accent04.normal,
- iconColor: theme.colors.error.default,
- });
- return;
- }
- }
+ createPredictTransactionStatusChangedHandler({
+ queryClient,
+ navigation,
+ theme,
+ normalizedSelectedAddress,
+ bottomSheetEnabled,
+ deposit,
+ claim,
+ withdraw,
+ withdrawTransactionAmount: withdrawTransaction?.amount,
+ getTransactionMetadata: (transactionId) =>
+ selectTransactionMetadataById(store.getState(), transactionId),
+ isPerpsPredictMoneyDeposit,
+ isPerpsPredictMoneyWithdraw,
+ shouldSuppressLegacyOrderFailureToast,
+ getWithdrawConfirmedMessage,
+ })(payload, showToast);
},
[
bottomSheetEnabled,
@@ -424,10 +94,7 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
navigation,
normalizedSelectedAddress,
queryClient,
- theme.colors.accent04.normal,
- theme.colors.error.default,
- theme.colors.primary.default,
- theme.colors.success.default,
+ theme,
withdraw,
withdrawTransaction?.amount,
],
@@ -443,41 +110,3 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
[handleTransactionStatusChanged],
);
};
-
-/**
- * Derives the withdraw-confirmed toast message from transaction metadata.
- *
- * For post-quote withdrawals with valid metamaskPay data, displays the
- * resolved token symbol and targetFiat amount. Otherwise falls back to the
- * original USDC-only message.
- */
-function getWithdrawConfirmedMessage(
- transactionId: string | undefined,
- fallbackAmount: number,
-): { title: string; description: string } {
- const title = strings('predict.withdraw.withdraw_completed');
-
- const { isPostQuote, targetFiat, tokenSymbol } = resolveWithdrawTokenInfo(
- store.getState(),
- transactionId,
- );
-
- if (!isPostQuote) {
- return {
- title,
- description: strings('predict.withdraw.withdraw_completed_subtitle', {
- amount: formatPrice(fallbackAmount),
- }),
- };
- }
-
- const withdrawAmount = targetFiat ?? fallbackAmount;
-
- return {
- title,
- description: strings(
- 'predict.withdraw.withdraw_any_token_completed_subtitle',
- { amount: formatPrice(withdrawAmount), token: tokenSymbol },
- ),
- };
-}
diff --git a/app/components/UI/Predict/utils/predictErrorHandler.test.ts b/app/components/UI/Predict/utils/predictErrorHandler.test.ts
index 57ea1253b4a..dd7f5005689 100644
--- a/app/components/UI/Predict/utils/predictErrorHandler.test.ts
+++ b/app/components/UI/Predict/utils/predictErrorHandler.test.ts
@@ -39,7 +39,6 @@ jest.mock('../../../../util/Logger', () => ({
const mockTheme = {
colors: {
error: { default: 'error-color' },
- accent04: { normal: 'accent-color' },
},
};
@@ -103,7 +102,6 @@ describe('predictErrorHandler', () => {
],
iconName: IconName.Error,
iconColor: 'error-color',
- backgroundColor: 'accent-color',
hasNoTimeout: false,
});
});
diff --git a/app/components/UI/Predict/utils/predictErrorHandler.ts b/app/components/UI/Predict/utils/predictErrorHandler.ts
index b6a3fe52bd2..a20637578a8 100644
--- a/app/components/UI/Predict/utils/predictErrorHandler.ts
+++ b/app/components/UI/Predict/utils/predictErrorHandler.ts
@@ -26,7 +26,7 @@ export function ensureError(error: unknown): Error {
export function createDepositErrorToast(
theme: {
- colors: { error: { default: string }; accent04: { normal: string } };
+ colors: { error: { default: string } };
},
onRetry?: () => void,
) {
@@ -42,7 +42,6 @@ export function createDepositErrorToast(
],
iconName: IconName.Error,
iconColor: theme.colors.error.default,
- backgroundColor: theme.colors.accent04.normal,
hasNoTimeout: false,
...(onRetry && {
linkButtonOptions: {
diff --git a/app/components/UI/Ramp/utils/v2OrderToast.test.ts b/app/components/UI/Ramp/utils/v2OrderToast.test.ts
index a77cc25c81e..b4fbc20e786 100644
--- a/app/components/UI/Ramp/utils/v2OrderToast.test.ts
+++ b/app/components/UI/Ramp/utils/v2OrderToast.test.ts
@@ -1,4 +1,5 @@
import { RampsOrderStatus } from '@metamask/ramps-controller';
+import { IconName } from '../../../../component-library/components/Icons/Icon';
import { ToastVariants } from '../../../../component-library/components/Toast/Toast.types';
import {
buildV2OrderToastOptions,
@@ -95,9 +96,11 @@ describe('v2OrderToast', () => {
const result = buildV2OrderToastOptions(params);
expect(result).not.toBeNull();
- expect(result?.variant).toBe(ToastVariants.Plain);
+ expect(result?.variant).toBe(ToastVariants.Icon);
expect(result?.hasNoTimeout).toBe(false);
- expect(result?.startAccessory).toBeDefined();
+ if (result?.variant === ToastVariants.Icon) {
+ expect(result.iconName).toBe(IconName.Confirmation);
+ }
expect(result?.labelOptions?.[0]?.label).toBe(
'Your purchase of 100.5 USDC was successful!',
);
@@ -122,9 +125,11 @@ describe('v2OrderToast', () => {
const result = buildV2OrderToastOptions(params);
expect(result).not.toBeNull();
- expect(result?.variant).toBe(ToastVariants.Plain);
+ expect(result?.variant).toBe(ToastVariants.Icon);
expect(result?.hasNoTimeout).toBe(false);
- expect(result?.startAccessory).toBeDefined();
+ if (result?.variant === ToastVariants.Icon) {
+ expect(result.iconName).toBe(IconName.Warning);
+ }
expect(result?.labelOptions?.[0]?.label).toBe('Purchase of BTC failed');
expect(result?.labelOptions?.[0]?.isBold).toBe(true);
expect(result?.descriptionOptions?.description).toBe(
@@ -146,9 +151,11 @@ describe('v2OrderToast', () => {
const result = buildV2OrderToastOptions(params);
expect(result).not.toBeNull();
- expect(result?.variant).toBe(ToastVariants.Plain);
+ expect(result?.variant).toBe(ToastVariants.Icon);
expect(result?.hasNoTimeout).toBe(false);
- expect(result?.startAccessory).toBeDefined();
+ if (result?.variant === ToastVariants.Icon) {
+ expect(result.iconName).toBe(IconName.Warning);
+ }
expect(result?.labelOptions?.[0]?.label).toBe(
'Your purchase was cancelled',
);
@@ -207,8 +214,10 @@ describe('v2OrderToast', () => {
expect(mockToastService.showToast).toHaveBeenCalledTimes(1);
const callArg = mockToastService.showToast.mock.calls[0][0];
- expect(callArg.variant).toBe(ToastVariants.Plain);
- expect(callArg.startAccessory).toBeDefined();
+ expect(callArg.variant).toBe(ToastVariants.Icon);
+ if (callArg.variant === ToastVariants.Icon) {
+ expect(callArg.iconName).toBe(IconName.Confirmation);
+ }
});
it('does not call ToastService.showToast for Created status', () => {
diff --git a/app/components/UI/Ramp/utils/v2OrderToast.ts b/app/components/UI/Ramp/utils/v2OrderToast.ts
index b4062ca3e2d..1aa8283bd5a 100644
--- a/app/components/UI/Ramp/utils/v2OrderToast.ts
+++ b/app/components/UI/Ramp/utils/v2OrderToast.ts
@@ -1,13 +1,14 @@
import React from 'react';
-import { StyleSheet, View } from 'react-native';
import {
- IconColor as ReactNativeDsIconColor,
- IconSize as ReactNativeDsIconSize,
+ IconColor as DsIconColor,
+ IconSize as DsIconSize,
Spinner,
} from '@metamask/design-system-react-native';
-import { lightTheme } from '@metamask/design-tokens';
import { strings } from '../../../../../locales/i18n';
-import { IconName } from '../../../../component-library/components/Icons/Icon';
+import {
+ IconColor,
+ IconName,
+} from '../../../../component-library/components/Icons/Icon';
import {
ToastOptions,
ToastVariants,
@@ -17,10 +18,6 @@ import Routes from '../../../../constants/navigation/Routes';
import NavigationService from '../../../../core/NavigationService';
import ToastService from '../../../../core/ToastService';
import { renderNumber } from '../../../../util/number';
-import Avatar, {
- AvatarSize,
- AvatarVariant,
-} from '../../../../component-library/components/Avatars/Avatar';
export interface V2OrderToastParams {
orderId: string;
@@ -29,23 +26,6 @@ export interface V2OrderToastParams {
status: RampsOrderStatus;
}
-const toastStyles = StyleSheet.create({
- spinnerContainer: {
- paddingTop: 4,
- marginRight: 12,
- alignSelf: 'flex-start',
- alignItems: 'flex-start',
- justifyContent: 'flex-start',
- },
- iconContainer: {
- paddingTop: 0,
- marginRight: 12,
- alignSelf: 'flex-start',
- alignItems: 'flex-start',
- justifyContent: 'flex-start',
- },
-});
-
/**
* Builds toast options for V2 Ramps orders based on order status.
* Returns null for statuses that don't warrant a toast (Created, Precreated, Unknown, IdExpired).
@@ -61,14 +41,10 @@ export function buildV2OrderToastOptions(
variant: ToastVariants.Plain,
hasNoTimeout: false,
customBottomOffset: 32,
- startAccessory: React.createElement(
- View,
- { style: toastStyles.spinnerContainer },
- React.createElement(Spinner, {
- color: ReactNativeDsIconColor.PrimaryDefault,
- spinnerIconProps: { size: ReactNativeDsIconSize.Xl },
- }),
- ),
+ startAccessory: React.createElement(Spinner, {
+ color: DsIconColor.IconDefault,
+ spinnerIconProps: { size: DsIconSize.Lg },
+ }),
labelOptions: [
{
label: strings('ramps_v2.notifications.purchase_pending_title', {
@@ -100,19 +76,11 @@ export function buildV2OrderToastOptions(
? renderNumber(String(cryptoAmount))
: '';
return {
- variant: ToastVariants.Plain,
+ variant: ToastVariants.Icon,
+ iconName: IconName.Confirmation,
+ iconColor: IconColor.Success,
+ backgroundColor: 'transparent',
hasNoTimeout: false,
- startAccessory: React.createElement(
- View,
- { style: toastStyles.iconContainer },
- React.createElement(Avatar, {
- variant: AvatarVariant.Icon,
- name: IconName.Confirmation,
- size: AvatarSize.Lg,
- iconColor: lightTheme.colors.success.default,
- backgroundColor: 'transparent',
- }),
- ),
labelOptions: [
{
label: strings('ramps_v2.notifications.purchase_completed_title', {
@@ -135,19 +103,11 @@ export function buildV2OrderToastOptions(
case RampsOrderStatus.Failed: {
return {
- variant: ToastVariants.Plain,
+ variant: ToastVariants.Icon,
+ iconName: IconName.Warning,
+ iconColor: IconColor.Error,
+ backgroundColor: 'transparent',
hasNoTimeout: false,
- startAccessory: React.createElement(
- View,
- { style: toastStyles.iconContainer },
- React.createElement(Avatar, {
- variant: AvatarVariant.Icon,
- name: IconName.Warning,
- size: AvatarSize.Lg,
- iconColor: lightTheme.colors.error.default,
- backgroundColor: 'transparent',
- }),
- ),
labelOptions: [
{
label: strings('ramps_v2.notifications.purchase_failed_title', {
@@ -166,19 +126,11 @@ export function buildV2OrderToastOptions(
case RampsOrderStatus.Cancelled: {
return {
- variant: ToastVariants.Plain,
+ variant: ToastVariants.Icon,
+ iconName: IconName.Warning,
+ iconColor: IconColor.Warning,
+ backgroundColor: 'transparent',
hasNoTimeout: false,
- startAccessory: React.createElement(
- View,
- { style: toastStyles.iconContainer },
- React.createElement(Avatar, {
- variant: AvatarVariant.Icon,
- name: IconName.Warning,
- size: AvatarSize.Lg,
- iconColor: lightTheme.colors.warning.default,
- backgroundColor: 'transparent',
- }),
- ),
labelOptions: [
{
label: strings('ramps_v2.notifications.purchase_cancelled_title'),
diff --git a/app/components/UI/Rewards/hooks/useRewardsToast.test.tsx b/app/components/UI/Rewards/hooks/useRewardsToast.test.tsx
index af2bd909c49..6281d6a3ae5 100644
--- a/app/components/UI/Rewards/hooks/useRewardsToast.test.tsx
+++ b/app/components/UI/Rewards/hooks/useRewardsToast.test.tsx
@@ -414,8 +414,6 @@ describe('useRewardsToast', () => {
{ label: 'Winner title', isBold: true },
]);
expect(config.startAccessory).toBeDefined();
- const { getByTestId } = render(config.startAccessory as ReactElement);
- expect(getByTestId('rewards-nudge-start-accessory-box')).toBeDefined();
config.linkButtonOptions?.onPress?.();
config.closeButtonOptions?.onPress?.();
expect(onCta).toHaveBeenCalledTimes(1);
diff --git a/app/components/UI/Rewards/hooks/useRewardsToast.tsx b/app/components/UI/Rewards/hooks/useRewardsToast.tsx
index 90ab7827b2e..d3b03bd355a 100644
--- a/app/components/UI/Rewards/hooks/useRewardsToast.tsx
+++ b/app/components/UI/Rewards/hooks/useRewardsToast.tsx
@@ -1,6 +1,5 @@
import React, { useCallback, useContext, useMemo } from 'react';
import { ActivityIndicator } from 'react-native';
-import { Box } from '@metamask/design-system-react-native';
import { ToastContext } from '../../../../component-library/components/Toast';
import {
ButtonIconVariant,
@@ -131,9 +130,7 @@ const useRewardsToast = (): {
hasNoTimeout: true,
hapticsType: NotificationMoment.Warning,
startAccessory: (
-
-
-
+
),
labelOptions: getRewardsToastLabels(title),
descriptionOptions: getRewardsToastDescriptionLabels(subtitle),
@@ -189,14 +186,12 @@ const useRewardsToast = (): {
hasNoTimeout: true,
hapticsType: NotificationMoment.Warning,
startAccessory: (
-
-
-
+
),
labelOptions: getRewardsToastLabels(
strings('rewards.notifications_nudge.title'),
@@ -225,14 +220,12 @@ const useRewardsToast = (): {
hasNoTimeout: true,
hapticsType: NotificationMoment.Success,
startAccessory: (
-
-
-
+
),
labelOptions: getRewardsToastLabels(title),
descriptionOptions: { description },
diff --git a/app/components/Views/Onboarding/index.tsx b/app/components/Views/Onboarding/index.tsx
index f03547fd015..d084d16952c 100644
--- a/app/components/Views/Onboarding/index.tsx
+++ b/app/components/Views/Onboarding/index.tsx
@@ -11,9 +11,8 @@ import {
BackHandler,
ScrollView,
InteractionManager,
- Animated,
- Easing,
Platform,
+ View,
} from 'react-native';
import { captureException } from '@sentry/react-native';
import { colors as importedColors } from '../../../styles/common';
@@ -22,7 +21,6 @@ import { useSelector, useDispatch } from 'react-redux';
import FadeOutOverlay from '../../UI/FadeOutOverlay';
import Device from '../../../util/device';
import BaseNotification from '../../../component-library/components-temp/BaseNotification';
-import ElevatedView from 'react-native-elevated-view';
import { loadingSet, loadingUnset } from '../../../actions/user';
import {
saveOnboardingEvent as saveEvent,
@@ -209,7 +207,8 @@ const Onboarding = () => {
startFoxAnimation: undefined,
});
- const notificationAnimated = useRef(new Animated.Value(100)).current;
+ const [onboardingNotificationVisible, setOnboardingNotificationVisible] =
+ useState(false);
const onboardingTraceCtx = useRef(undefined);
const socialLoginTraceCtx = useRef(undefined);
@@ -227,19 +226,6 @@ const Onboarding = () => {
const mounted = useRef(false);
const hasCheckedVaultBackup = useRef(false);
const warningCallback = useRef<() => boolean>(() => true);
- const notificationTimer = useRef | null>(null);
-
- const animatedTimingStart = useCallback(
- (animatedRef: Animated.Value, toValue: number): void => {
- Animated.timing(animatedRef, {
- toValue,
- duration: 500,
- easing: Easing.linear,
- useNativeDriver: true,
- }).start();
- },
- [],
- );
const disableBackPress = useCallback((): void => {
// Disable back press
@@ -248,12 +234,9 @@ const Onboarding = () => {
}, []);
const showNotification = useCallback((): void => {
- animatedTimingStart(notificationAnimated, 0);
- notificationTimer.current = setTimeout(() => {
- animatedTimingStart(notificationAnimated, 200);
- }, 4000);
+ setOnboardingNotificationVisible(true);
disableBackPress();
- }, [animatedTimingStart, notificationAnimated, disableBackPress]);
+ }, [disableBackPress]);
const updateNavBar = useCallback((): void => {
navigation.setOptions({
@@ -1105,27 +1088,23 @@ const Onboarding = () => {
};
return (
-
-
-
-
-
+ setOnboardingNotificationVisible(false)}
+ status="success"
+ data={notificationData}
+ />
+
);
}, [
route?.params?.delete,
route?.params?.showErrorReportSentToast,
- notificationAnimated,
+ onboardingNotificationVisible,
tw,
]);
@@ -1156,9 +1135,6 @@ const Onboarding = () => {
return () => {
mounted.current = false;
- if (notificationTimer.current) {
- clearTimeout(notificationTimer.current);
- }
unsetLoading();
InteractionManager.runAfterInteractions(PreventScreenshot.allow);
};
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyToastOptions.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyToastOptions.tsx
index c2b246b9f96..b3e21c907eb 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyToastOptions.tsx
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyToastOptions.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import {
- Box,
IconColor as DsIconColor,
IconSize as DsIconSize,
Spinner,
@@ -85,12 +84,10 @@ export function buildQuickBuyToastOptions(
descriptionOptions,
closeButtonOptions,
startAccessory: (
-
-
-
+
),
};
}
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.test.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.test.tsx
index ae35e52b56a..9cabfb845e1 100644
--- a/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.test.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.test.tsx
@@ -156,9 +156,10 @@ describe('GasFeeTokenToast', () => {
expect(mockShowToast).toHaveBeenCalledWith(
expect.objectContaining({
labelOptions: [
- { label: "You're paying this network fee with ", isBold: false },
- { label: matchingTokenSymbol, isBold: true },
- { label: '.', isBold: false },
+ {
+ label: `You're paying this network fee with ${matchingTokenSymbol}.`,
+ isBold: true,
+ },
],
variant: ToastVariants.Network,
networkImageSource: { uri: matchingTokenImage },
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.tsx
index 708153ee740..d6c7e40438e 100644
--- a/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.tsx
@@ -2,6 +2,7 @@ import { useContext, useEffect, useRef } from 'react';
import {
ToastContext,
ToastVariants,
+ ButtonIconVariant,
} from '../../../../../../component-library/components/Toast';
import { strings } from '../../../../../../../locales/i18n';
import { TransactionType } from '@metamask/transaction-controller';
@@ -13,7 +14,6 @@ import {
} from '../../../hooks/gas/useGasFeeToken';
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
import { IconName } from '../../../../../../component-library/components/Icons/Icon';
-import { ButtonVariants } from '../../../../../../component-library/components/Buttons/Button';
import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
import { getNetworkImageSource } from '../../../../../../util/networks';
import { hasTransactionType } from '../../../utils/transaction';
@@ -52,9 +52,10 @@ export function GasFeeTokenToast() {
toast.showToast({
labelOptions: [
- { label: strings('gas_fee_token_toast.message'), isBold: false },
- { label: `${gasFeeToken.symbol}`, isBold: true },
- { label: '.', isBold: false },
+ {
+ label: `${strings('gas_fee_token_toast.message')}${gasFeeToken.symbol}.`,
+ isBold: true,
+ },
],
variant: ToastVariants.Network,
hasNoTimeout: false,
@@ -63,18 +64,11 @@ export function GasFeeTokenToast() {
? { uri: tokenSelected.image }
: networkImageSource,
closeButtonOptions: {
- variant: ButtonVariants.Primary,
- endIconName: IconName.Close,
- label: undefined,
+ variant: ButtonIconVariant.Icon,
+ iconName: IconName.Close,
onPress: () => {
toast?.closeToast();
},
- style: {
- backgroundColor: 'transparent',
- paddingHorizontal: 4,
- paddingVertical: 10,
- height: 20,
- },
},
});
}, [