diff --git a/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx b/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx index 5febaf2a16c4..2c3960178aa6 100644 --- a/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx +++ b/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react-native'; +import { fireEvent, render, waitFor } from '@testing-library/react-native'; import { AccountGroupId, AccountWalletId } from '@metamask/account-api'; import { SolAccountType, EthScope, SolScope } from '@metamask/keyring-api'; +import { IconName, toast } from '@metamask/design-system-react-native'; import { createMockInternalAccount } from '../../../../util/test/accountsControllerTestUtils'; import renderWithProvider from '../../../../util/test/renderWithProvider'; @@ -10,6 +11,8 @@ import { AddressList } from './AddressList'; import { MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID } from '../../../../component-library/components-temp/MultichainAccounts/MultichainAddressRow'; import { toFormattedAddress } from '../../../../util/address'; import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events'; +import { strings } from '../../../../../locales/i18n'; +import { AddressListIds } from './AddressList.testIds'; const ACCOUNT_WALLET_ID = 'entropy:wallet-id-1' as AccountWalletId; const ACCOUNT_GROUP_ID = 'entropy:wallet-id-1/1' as AccountGroupId; @@ -55,6 +58,20 @@ jest.mock('../../../../core/ClipboardManager', () => ({ setString: jest.fn(), })); +jest.mock('@metamask/design-system-react-native', () => { + const actualDesignSystem = jest.requireActual( + '@metamask/design-system-react-native', + ); + + return { + ...actualDesignSystem, + Toaster: jest.fn(() => null), + toast: Object.assign(jest.fn(), { + dismiss: jest.fn(), + }), + }; +}); + const mockEthEoaAccount = { ...createMockInternalAccount( '0x4fec2622fb662e892dd0e5060b91fa49ddcfdcb5', @@ -193,6 +210,27 @@ describe('AddressList', () => { }); }); + it('calls navigation.goBack from the header back button', () => { + renderWithAddressList(); + + const navOptionsWithHeader = mockSetOptions.mock.calls + .map(([opts]) => opts) + .find( + (opts) => + opts && + opts.headerShown === true && + typeof opts.header === 'function', + ); + + expect(navOptionsWithHeader).toBeDefined(); + + const { getByTestId, unmount } = render(navOptionsWithHeader.header()); + fireEvent.press(getByTestId(AddressListIds.GO_BACK)); + + expect(mockGoBack).toHaveBeenCalled(); + unmount(); + }); + it('does not set navigation options when title is not provided', () => { const { useParams } = jest.requireMock( '../../../../util/navigation/navUtils', @@ -307,5 +345,21 @@ describe('AddressList', () => { expect(addPropertiesCall).toHaveProperty('location', 'address-list'); }); + + it('shows the design system copy toast', async () => { + const { getAllByTestId } = renderWithAddressList(); + + const copyButton = getAllByTestId( + 'multichain-address-row-copy-button', + )[0]; + fireEvent.press(copyButton); + + await waitFor(() => { + expect(toast).toHaveBeenCalledWith({ + description: strings('notifications.address_copied_to_clipboard'), + hasNoTimeout: false, + }); + }); + }); }); }); diff --git a/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx b/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx index 18b112e5b45a..8bceddcd5231 100644 --- a/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx +++ b/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useLayoutEffect } from 'react'; +import React, { useCallback, useLayoutEffect } from 'react'; import { View } from 'react-native'; import { useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; @@ -7,7 +7,7 @@ import { FlashList } from '@shopify/flash-list'; import { useStyles } from '../../../hooks/useStyles'; import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics'; import { selectInternalAccountListSpreadByScopesByGroupId } from '../../../../selectors/multichainAccounts/accounts'; -import { IconName } from '@metamask/design-system-react-native'; +import { IconName, Toaster, toast } from '@metamask/design-system-react-native'; import MultichainAddressRow, { MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID, } from '../../../../component-library/components-temp/MultichainAccounts/MultichainAddressRow'; @@ -22,7 +22,6 @@ import styleSheet from './styles'; import type { AddressListProps, AddressItem } from './types'; import ClipboardManager from '../../../../core/ClipboardManager'; import getHeaderCompactStandardNavbarOptions from '../../../../component-library/components-temp/HeaderCompactStandard/getHeaderCompactStandardNavbarOptions'; -import { ToastContext } from '../../../../component-library/components/Toast'; import { strings } from '../../../../../locales/i18n'; import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events'; @@ -39,7 +38,6 @@ export const createAddressListNavigationDetails = export const AddressList = () => { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); - const { toastRef } = useContext(ToastContext); const { trackEvent, createEventBuilder } = useAnalytics(); const { groupId, title, onLoad } = useParams(); @@ -71,8 +69,15 @@ export const AddressList = () => { address={item.account.address} copyParams={{ toastMessage: strings('notifications.address_copied_to_clipboard'), - callback: copyAddressToClipboard, - toastRef, + callback: async () => { + await copyAddressToClipboard(); + toast({ + description: strings( + 'notifications.address_copied_to_clipboard', + ), + hasNoTimeout: false, + }); + }, }} icons={[ { @@ -98,7 +103,7 @@ export const AddressList = () => { /> ); }, - [navigation, groupId, toastRef, trackEvent, createEventBuilder], + [navigation, groupId, trackEvent, createEventBuilder], ); useLayoutEffect(() => { @@ -123,6 +128,7 @@ export const AddressList = () => { renderItem={renderAddressItem} onLoad={onLoad} /> + ); }; diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx index c33a8dab3c3f..7e01c4bcfdcb 100644 --- a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx @@ -15,31 +15,28 @@ const mockDispatch = jest.fn(); // Mock the BottomSheet component const mockOnCloseBottomSheet = jest.fn(); // eslint-disable-next-line import-x/no-commonjs -jest.mock( - '../../../../component-library/components/BottomSheets/BottomSheet', - () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports, import-x/no-commonjs, @typescript-eslint/no-var-requires - const ReactMock = require('react'); - return { - __esModule: true, - default: ReactMock.forwardRef( - ( - { children }: { children: React.ReactNode }, - ref: React.Ref<{ onCloseBottomSheet: () => void }>, - ) => { - ReactMock.useImperativeHandle(ref, () => ({ - onCloseBottomSheet: mockOnCloseBottomSheet, - })); - return ReactMock.createElement( - 'View', - { testID: 'bottom-sheet' }, - children, - ); - }, - ), - }; - }, -); +jest.mock('@metamask/design-system-react-native', () => { + const actualDesignSystem = jest.requireActual( + '@metamask/design-system-react-native', + ); + // eslint-disable-next-line @typescript-eslint/no-require-imports, import-x/no-commonjs, @typescript-eslint/no-var-requires + const ReactMock = require('react'); + + return { + ...actualDesignSystem, + BottomSheet: ReactMock.forwardRef( + ( + { children, testID }: { children: React.ReactNode; testID?: string }, + ref: React.Ref<{ onCloseBottomSheet: () => void }>, + ) => { + ReactMock.useImperativeHandle(ref, () => ({ + onCloseBottomSheet: mockOnCloseBottomSheet, + })); + return ReactMock.createElement('View', { testID }, children); + }, + ), + }; +}); // Mock React Navigation jest.mock('@react-navigation/native', () => { diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx index ae95fb07b9fc..daad79080bb7 100644 --- a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx @@ -10,10 +10,9 @@ import { Button, ButtonVariant, ButtonBaseSize, + BottomSheet, + type BottomSheetRef, } from '@metamask/design-system-react-native'; -import BottomSheet, { - BottomSheetRef, -} from '../../../../component-library/components/BottomSheets/BottomSheet'; import { useNavigation, useTheme } from '@react-navigation/native'; import { strings } from '../../../../../locales/i18n'; import { useStyles } from '../../../../component-library/hooks'; @@ -63,7 +62,11 @@ const LearnMoreBottomSheet: React.FC = ({ }, [isCheckboxChecked, navigation, isBasicFunctionalityEnabled, dispatch]); return ( - + ( - - - - - - - + + + + + + ), ], diff --git a/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.tsx b/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.tsx index e713298b0080..79df1f7cd17e 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.tsx +++ b/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnect.tsx @@ -2,7 +2,6 @@ import { useNavigation } from '@react-navigation/native'; import React, { useCallback, - useContext, useEffect, useMemo, useRef, @@ -15,10 +14,12 @@ import { NON_EVM_TESTNET_IDS } from '@metamask/multichain-network-controller'; // External dependencies. import { strings } from '../../../../../locales/i18n.js'; import { - ToastContext, - ToastVariants, -} from '../../../../component-library/components/Toast/index.ts'; -import { ToastOptions } from '../../../../component-library/components/Toast/Toast.types.ts'; + AvatarFavicon, + AvatarFaviconSize, + Box, + Toaster, + toast, +} from '@metamask/design-system-react-native'; import { USER_INTENT } from '../../../../constants/permissions.ts'; import { MetaMetricsEvents } from '../../../../core/Analytics/index.ts'; import Engine from '../../../../core/Engine/index.ts'; @@ -49,12 +50,13 @@ import useFavicon from '../../../hooks/useFavicon/useFavicon.ts'; import { AccountConnectProps, AccountConnectScreens, + NetworkAvatarProps, } from '../../AccountConnect/AccountConnect.types.ts'; -import { getNetworkImageSource } from '../../../../util/networks/index.js'; import { AvatarSize, AvatarVariant, -} from '../../../../component-library/components/Avatars/Avatar/index.ts'; +} from '../../../../component-library/components/Avatars/Avatar'; +import { getNetworkImageSource } from '../../../../util/networks/index.js'; import { EvmAndMultichainNetworkConfigurationsWithCaipChainId, getSelectedMultichainNetwork, @@ -97,7 +99,6 @@ import { getPermissions } from '../../../../selectors/snaps/index.ts'; import { useSDKV2Connection } from '../../../hooks/useSDKV2Connection'; import { useAccountGroupsForPermissions } from '../../../hooks/useAccountGroupsForPermissions/useAccountGroupsForPermissions.ts'; import NetworkConnectMultiSelector from '../../NetworkConnect/NetworkConnectMultiSelector/index.ts'; -import { Box } from '@metamask/design-system-react-native'; import { TESTNET_CAIP_IDS } from '../../../../constants/network.js'; import { getCaip25AccountIdsFromAccountGroupAndScope } from '../../../../util/multichain/getCaip25AccountIdsFromAccountGroupAndScope.ts'; import { isSnapId } from '@metamask/snaps-utils'; @@ -124,6 +125,9 @@ const ScreenContainer: React.FC = ({ ); +const NETWORK_AVATAR_SIZE = AvatarSize.Xs; +const NETWORK_AVATAR_VARIANT = AvatarVariant.Network; + const MultichainAccountConnect = (props: AccountConnectProps) => { const { colors } = useTheme(); const { styles } = useStyles(styleSheet, {}); @@ -397,13 +401,15 @@ const MultichainAccountConnect = (props: AccountConnectProps) => { .filter( (selectedChainId) => !NON_EVM_TESTNET_IDS.includes(selectedChainId), ) - .map((selectedChainId) => ({ - size: AvatarSize.Xs, - name: networkConfigurations[selectedChainId]?.name || '', - imageSource: getNetworkImageSource({ chainId: selectedChainId }), - variant: AvatarVariant.Network, - caipChainId: selectedChainId, - })), + .map( + (selectedChainId): NetworkAvatarProps => ({ + size: NETWORK_AVATAR_SIZE, + name: networkConfigurations[selectedChainId]?.name || '', + imageSource: getNetworkImageSource({ chainId: selectedChainId }), + variant: NETWORK_AVATAR_VARIANT, + caipChainId: selectedChainId, + }), + ), [networkConfigurations, selectedChainIds], ); @@ -471,8 +477,6 @@ const MultichainAccountConnect = (props: AccountConnectProps) => { const [userIntent, setUserIntent] = useState(USER_INTENT.None); const isMountedRef = useRef(true); - const { toastRef } = useContext(ToastContext); - const accountsLength = useSelector(selectAccountsLength); const dappUrl = @@ -714,15 +718,14 @@ const MultichainAccountConnect = (props: AccountConnectProps) => { .build(), ); - const labelOptions: ToastOptions['labelOptions'] = - connectedAccountLength >= 1 - ? [{ label: strings('toast.permissions_updated') }] - : []; - - toastRef?.current?.showToast({ - variant: ToastVariants.App, - labelOptions, - appIconSource: faviconSource, + toast({ + description: + connectedAccountLength >= 1 + ? strings('toast.permissions_updated') + : undefined, + startAccessory: ( + + ), hasNoTimeout: false, }); } catch (e) { @@ -745,7 +748,6 @@ const MultichainAccountConnect = (props: AccountConnectProps) => { createEventBuilder, accountsLength, originSource, - toastRef, faviconSource, referrer, ]); @@ -996,6 +998,7 @@ const MultichainAccountConnect = (props: AccountConnectProps) => { )} {renderPhishingModal()} + ); }; diff --git a/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx b/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx index 7101315a946c..ec2878dfca55 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx +++ b/app/components/Views/MultichainAccounts/MultichainAccountConnect/MultichainAccountConnectMultiSelector/MultichainAccountConnectMultiSelector.tsx @@ -9,11 +9,10 @@ import { Button, ButtonVariant, ButtonBaseSize, + Text, + TextColor, } from '@metamask/design-system-react-native'; import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader'; -import Text, { - TextColor, -} from '../../../../../component-library/components/Texts/Text'; import { useStyles } from '../../../../../component-library/hooks'; import HelpText, { HelpTextSeverity, @@ -179,7 +178,7 @@ const MultichainAccountConnectMultiSelector = ({ {connection?.originatorInfo?.apiVersion && ( - + {strings('permissions.sdk_connection', { originator_platform: connection?.originatorInfo?.platform, api_version: connection?.originatorInfo?.apiVersion, diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx index 33d693e2de4c..56daff830b2e 100644 --- a/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx +++ b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native'; import { Platform } from 'react-native'; import { AccountGroupId, AccountWalletId } from '@metamask/account-api'; import { SolAccountType, EthScope, SolScope } from '@metamask/keyring-api'; +import { toast } from '@metamask/design-system-react-native'; import { createMockInternalAccount } from '../../../../util/test/accountsControllerTestUtils'; import { renderScreen } from '../../../../util/test/renderWithProvider'; @@ -63,6 +64,24 @@ jest.mock('../../../../core/Engine', () => ({ }, })); +jest.mock('../../../../core/ClipboardManager', () => ({ + setStringExpire: jest.fn(), +})); + +jest.mock('@metamask/design-system-react-native', () => { + const actualDesignSystem = jest.requireActual( + '@metamask/design-system-react-native', + ); + + return { + ...actualDesignSystem, + Toaster: jest.fn(() => null), + toast: Object.assign(jest.fn(), { + dismiss: jest.fn(), + }), + }; +}); + const mockEthEoaAccount = { ...createMockInternalAccount( '0x4fec2622fb662e892dd0e5060b91fa49ddcfdcb5', @@ -239,6 +258,39 @@ describe('PrivateKeyList', () => { unmount(); }); + it('copies a private key and shows the design system toast', async () => { + const { getByTestId, findByTestId, getAllByTestId } = + renderWithPrivateKeyList(); + const mockClipboardManager = jest.requireMock( + '../../../../core/ClipboardManager', + ) as { setStringExpire: jest.Mock }; + + fireEvent.changeText( + getByTestId(PrivateKeyListIds.PASSWORD_INPUT), + 'correct-password', + ); + fireEvent.press(getByTestId(PrivateKeyListIds.CONTINUE_BUTTON)); + + await findByTestId(PrivateKeyListIds.LIST); + + fireEvent.press( + getAllByTestId(PrivateKeyListIds.COPY_TO_CLIPBOARD_BUTTON)[0], + ); + + await waitFor(() => { + expect(mockClipboardManager.setStringExpire).toHaveBeenCalledWith( + `mock-private-key-for-${mockEthEoaAccount.address}`, + ); + }); + + await waitFor(() => { + expect(toast).toHaveBeenCalledWith({ + description: strings('multichain_accounts.private_key_list.copied'), + hasNoTimeout: false, + }); + }); + }); + it('clears wrong-password error and shows list when correct password is entered after wrong', async () => { const { getByTestId, findByTestId, queryByTestId } = renderWithPrivateKeyList(); diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx index 0895a47575cf..c90ae7e431d8 100644 --- a/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx +++ b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useCallback, useMemo, - useContext, useLayoutEffect, } from 'react'; import { TextInput, Linking } from 'react-native'; @@ -26,12 +25,13 @@ import { Button, ButtonVariant, ButtonSize, + Toaster, + toast, } from '@metamask/design-system-react-native'; import Engine from '../../../../core/Engine'; import ClipboardManager from '../../../../core/ClipboardManager'; import MultichainAddressRow from '../../../../component-library/components-temp/MultichainAccounts/MultichainAddressRow'; import getHeaderCompactStandardNavbarOptions from '../../../../component-library/components-temp/HeaderCompactStandard/getHeaderCompactStandardNavbarOptions'; -import { ToastContext } from '../../../../component-library/components/Toast'; import { strings } from '../../../../../locales/i18n'; import { useParams, @@ -75,7 +75,6 @@ export const PrivateKeyList = () => { const theme = useTheme(); const { bottom: bottomInset } = useSafeAreaInsets(); - const { toastRef } = useContext(ToastContext); const [password, setPassword] = useState(''); const [wrongPassword, setWrongPassword] = useState(false); const [reveal, setReveal] = useState(false); @@ -185,16 +184,21 @@ export const PrivateKeyList = () => { address={item.account.address} copyParams={{ toastMessage: strings('multichain_accounts.private_key_list.copied'), - toastRef, callback: async () => { await ClipboardManager.setStringExpire( privateKeys[item.account.id], ); + toast({ + description: strings( + 'multichain_accounts.private_key_list.copied', + ), + hasNoTimeout: false, + }); }, }} /> ), - [privateKeys, toastRef], + [privateKeys], ); const privateKeyBannerDescription = useMemo( @@ -323,6 +327,7 @@ export const PrivateKeyList = () => { /> {reveal ? renderPrivateKeyList() : renderPassword()} + ); };