diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts b/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts index 9bf03d0adbc..d4d99c40475 100644 --- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts +++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts @@ -3,7 +3,6 @@ import { StyleSheet, ViewStyle } from 'react-native'; // External dependencies. import { Theme } from '../../../../util/theme/models'; -import { fontStyles } from '../../../../styles/common'; // Internal dependencies. import { PickerAccountStyleSheetVars } from './PickerAccount.types'; @@ -24,34 +23,39 @@ const styleSheet = (params: { const { colors } = theme; const { style, cellAccountContainerStyle } = vars; return StyleSheet.create({ - base: Object.assign({} as ViewStyle, style) as ViewStyle, + base: { + ...(style as ViewStyle), + flexDirection: 'row', + padding: 0, + borderWidth: 0, + }, accountAvatar: { - marginRight: 16, + marginRight: 8, }, accountAddressLabel: { color: colors.text.alternative, + textAlign: 'center', }, cellAccount: { - flex: 1, flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', ...cellAccountContainerStyle, }, accountNameLabel: { + alignItems: 'center', + justifyContent: 'center', + }, + accountNameAvatar: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-start', }, - accountNameLabelText: { - marginTop: 4, - marginHorizontal: 5, - paddingHorizontal: 5, - ...fontStyles.bold, - color: colors.text.alternative, - borderWidth: 1, - borderRadius: 10, - borderColor: colors.border.default, + pickerAccountContainer: { justifyContent: 'center', - textAlign: 'center', + alignItems: 'center', + }, + dropDownIcon: { + marginLeft: 8, }, }); }; diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx index 4093e7da1ae..0bec81483f0 100644 --- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx +++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx @@ -9,6 +9,7 @@ import Avatar, { AvatarSize, AvatarVariant } from '../../Avatars/Avatar'; import Text, { TextVariant } from '../../Texts/Text'; import { formatAddress } from '../../../../util/address'; import { useStyles } from '../../../hooks'; +import { IconSize } from '../../Icons/Icon'; // Internal dependencies. import PickerBase from '../PickerBase'; @@ -25,7 +26,6 @@ const PickerAccount: React.ForwardRefRenderFunction< accountAddress, accountName, accountAvatarType, - accountTypeLabel, showAddress = true, cellAccountContainerStyle = {}, ...props @@ -40,33 +40,46 @@ const PickerAccount: React.ForwardRefRenderFunction< const renderCellAccount = () => ( - - - {accountName} - - {showAddress && ( - - {shortenedAddress} + + + + {accountName} - )} + ); return ( - - {renderCellAccount()} - + + + {renderCellAccount()} + + {showAddress && ( + + {shortenedAddress} + + )} + ); }; diff --git a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap index 0afdb8affeb..a078072c4da 100644 --- a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap +++ b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap @@ -1,223 +1,242 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PickerAccount should render correctly 1`] = ` - - - - - - - + + + + + + + + + + - - + } + testID="account-label" + > + Orangefox.eth + + - - - Orangefox.eth - - - 0x2990...a21a - - - - + + - + > + 0x2990...a21a + + `; diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx index 948721d6ac4..65666c2e125 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx @@ -45,6 +45,20 @@ describe('PickerNetwork', () => { ).toBeNull(); }); + it('shows network name when hideNetworkName is false', () => { + const { queryByTestId } = render( + , + ); + + expect( + queryByTestId(WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT), + ).not.toBeNull(); + }); + it('calls onPress when pressed', () => { const onPress = jest.fn(); const { getByTestId } = render( diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx index 1b4642ba967..29c48def333 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx @@ -34,6 +34,8 @@ const PickerNetwork = ({ size={AvatarSize.Xs} name={label} imageSource={imageSource} + testID={WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER} + accessibilityLabel={label} /> {hideNetworkName ? null : ( diff --git a/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap b/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap index f59fab74933..c2965c38bb0 100644 --- a/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap +++ b/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap @@ -19,6 +19,7 @@ exports[`PickerNetwork renders correctly 1`] = ` style={null} > { { }; }); +const mockNavigate = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + const initialState = { engine: { backgroundState: { diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index 8d990dee513..30b8241836f 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -1,15 +1,18 @@ // Third party dependencies. import React, { useCallback, useRef } from 'react'; -import { Alert, ListRenderItem, View } from 'react-native'; +import { Alert, ListRenderItem, View, ViewStyle } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { Hex } from '@metamask/utils'; // External dependencies. +import { selectInternalAccounts } from '../../../selectors/accountsController'; import Cell, { CellVariant, } from '../../../component-library/components/Cells/Cell'; +import { InternalAccount } from '@metamask/keyring-api'; import { useStyles } from '../../../component-library/hooks'; import { TextColor } from '../../../component-library/components/Texts/Text'; import SensitiveText, { @@ -28,11 +31,13 @@ import { AvatarVariant } from '../../../component-library/components/Avatars/Ava import { Account, Assets } from '../../hooks/useAccounts'; import UntypedEngine from '../../../core/Engine'; import { removeAccountsFromPermissions } from '../../../core/Permissions'; +import Routes from '../../../constants/navigation/Routes'; // Internal dependencies. import { AccountSelectorListProps } from './AccountSelectorList.types'; import styleSheet from './AccountSelectorList.styles'; import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; const AccountSelectorList = ({ onSelectAccount, @@ -49,6 +54,7 @@ const AccountSelectorList = ({ privacyMode = false, ...props }: AccountSelectorListProps) => { + const { navigate } = useNavigation(); // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const Engine = UntypedEngine as any; @@ -64,6 +70,8 @@ const AccountSelectorList = ({ ? AvatarAccountType.Blockies : AvatarAccountType.JazzIcon, ); + + const internalAccounts = useSelector(selectInternalAccounts); const getKeyExtractor = ({ address }: Account) => address; const renderAccountBalances = useCallback( @@ -169,6 +177,23 @@ const AccountSelectorList = ({ ], ); + const onNavigateToAccountActions = useCallback( + (selectedAccount: string) => { + const account = internalAccounts.find( + (accountData: InternalAccount) => + accountData.address.toLowerCase() === selectedAccount.toLowerCase(), + ); + + if (!account) return; + + navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.ACCOUNT_ACTIONS, + params: { selectedAccount: account }, + }); + }, + [navigate, internalAccounts], + ); + const renderAccountItem: ListRenderItem = useCallback( ({ item: { name, address, assets, type, isSelected, balanceError }, @@ -182,7 +207,7 @@ const AccountSelectorList = ({ const isDisabled = !!balanceError || isLoading || isSelectionDisabled; const cellVariant = isMultiSelect ? CellVariant.MultiSelect - : CellVariant.Select; + : CellVariant.SelectWithMenu; let isSelectedAccount = isSelected; if (selectedAddresses) { const lowercasedSelectedAddresses = selectedAddresses.map( @@ -193,12 +218,16 @@ const AccountSelectorList = ({ ); } - const cellStyle = { + const cellStyle: ViewStyle = { opacity: isLoading ? 0.5 : 1, }; + if (!isMultiSelect) { + cellStyle.alignItems = 'center'; + } return ( { onLongPress({ address, @@ -211,6 +240,7 @@ const AccountSelectorList = ({ isSelected={isSelectedAccount} title={accountName} secondaryText={shortAddress} + showSecondaryTextIcon={false} tertiaryText={balanceError} onPress={() => onSelectAccount?.(address, isSelectedAccount)} avatarProps={{ @@ -221,6 +251,10 @@ const AccountSelectorList = ({ tagLabel={tagLabel} disabled={isDisabled} style={cellStyle} + buttonProps={{ + onButtonClick: () => onNavigateToAccountActions(address), + buttonTestId: `${WalletViewSelectorsIDs.ACCOUNT_ACTIONS}-${index}`, + }} > {renderRightAccessory?.(address, accountName) || (assets && renderAccountBalances(assets, address))} @@ -228,6 +262,7 @@ const AccountSelectorList = ({ ); }, [ + onNavigateToAccountActions, accountAvatarType, onSelectAccount, renderAccountBalances, diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap index 036a2be8d53..dd4954812d8 100644 --- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap +++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap @@ -57,591 +57,772 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` onLayout={[Function]} style={null} > - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 1 - - - 0xC495...D272 - - - - $3200.00 + Account 1 - - 1 ETH - + + 0xC495...D272 + + + + + + + $3200.00 + + + 1 ETH + + - + + + + - + testID="main-wallet-account-actions-0" + > + + - + - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 2 - - - 0xd018...78E7 - - - - $6400.00 + Account 2 - - 2 ETH - + + 0xd018...78E7 + + + + + + + $6400.00 + + + 2 ETH + + + + + + + - + @@ -704,492 +885,672 @@ exports[`AccountSelectorList renders all accounts with right accessory 1`] = ` onLayout={[Function]} style={null} > - - - - - - + - - - - - - - + + + + + + + - Account 1 - - + Account 1 + + + + 0xC495...D272 + + + + - 0xC495...D272 - - - - - 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1 + + 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1 + + + + + + - + - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 2 - - + Account 2 + + + + 0xd018...78E7 + + + + - 0xd018...78E7 - - - - - 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2 + + 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2 + + + + + + - + @@ -1252,591 +1613,772 @@ exports[`AccountSelectorList renders correctly 1`] = ` onLayout={[Function]} style={null} > - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 1 - - - 0xC495...D272 - - - - $3200.00 + Account 1 - - 1 ETH - + + 0xC495...D272 + + + + + + + $3200.00 + + + 1 ETH + + - + + + + - + testID="main-wallet-account-actions-0" + > + + - + - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 2 - - - 0xd018...78E7 - - - - $6400.00 + Account 2 - - 2 ETH - + + 0xd018...78E7 + + + + + + + $6400.00 + + + 2 ETH + + + + + + + - + @@ -1896,309 +2438,490 @@ exports[`AccountSelectorList should render all accounts but only the balance for onLayout={[Function]} style={null} > - - - - - - - Account 1 - - + style={ + { + "flex": 1, + } + } + /> + - $3200.00 + Account 1 - - 1 ETH - + + 0xC495...D272 + + + + + + + $3200.00 + + + 1 ETH + + - + + + + - + testID="main-wallet-account-actions-0" + > + + - + - - - - - - - Account 2 - - + + - 0xd018...78E7 - + + Account 2 + + + + 0xd018...78E7 + + + + + + + + - + diff --git a/app/components/UI/AddressCopy/AddressCopy.styles.ts b/app/components/UI/AddressCopy/AddressCopy.styles.ts index 089c48d5136..46d5ba9d066 100644 --- a/app/components/UI/AddressCopy/AddressCopy.styles.ts +++ b/app/components/UI/AddressCopy/AddressCopy.styles.ts @@ -1,26 +1,14 @@ import { StyleSheet } from 'react-native'; -// External dependencies. -import { Theme } from '../../../util/theme/models'; -const styleSheet = (params: { theme: Theme }) => { - const { theme } = params; - const { colors } = theme; - - return StyleSheet.create({ +const styleSheet = () => + StyleSheet.create({ address: { flexDirection: 'row', alignItems: 'center', }, copyButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.primary.muted, - borderRadius: 20, - paddingHorizontal: 12, padding: 4, - marginLeft: 12, }, - icon: { marginLeft: 4 }, }); -}; + export default styleSheet; diff --git a/app/components/UI/AddressCopy/AddressCopy.tsx b/app/components/UI/AddressCopy/AddressCopy.tsx index d295c5f75ad..a99e46a4727 100644 --- a/app/components/UI/AddressCopy/AddressCopy.tsx +++ b/app/components/UI/AddressCopy/AddressCopy.tsx @@ -3,12 +3,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; // External dependencies -import Text, { - TextColor, - TextVariant, -} from '../../../component-library/components/Texts/Text'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { formatAddress } from '../../../util/address'; import Icon, { IconColor, IconName, @@ -25,12 +20,11 @@ import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletV // Internal dependencies import styleSheet from './AddressCopy.styles'; -import { AddressCopyProps } from './AddressCopy.types'; import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { toChecksumHexAddress } from '@metamask/controller-utils'; -const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { +const AddressCopy = () => { const { styles } = useStyles(styleSheet, {}); const dispatch = useDispatch(); @@ -69,28 +63,15 @@ const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { }; return ( - - {strings('asset_overview.address')}: - - - {selectedInternalAccount - ? formatAddress(selectedInternalAccount.address, formatAddressType) - : null} - diff --git a/app/components/UI/AddressCopy/AddressCopy.types.ts b/app/components/UI/AddressCopy/AddressCopy.types.ts deleted file mode 100644 index 6efa05bbea0..00000000000 --- a/app/components/UI/AddressCopy/AddressCopy.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface AddressCopyProps { - formatAddressType?: 'short' | 'mid' | 'full'; -} diff --git a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx index ed1a2f5fdc3..99054057d7e 100644 --- a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx +++ b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx @@ -44,6 +44,7 @@ jest.mock('../../../util/navigation/navUtils', () => ({ ticker: 'ETH', addTokenList: jest.fn(), }), + createNavigationDetails: jest.fn(), })); const mockUseBalanceInitialValue: Partial> = { @@ -101,14 +102,12 @@ describe('ConfirmAddAsset', () => { expect(getByText('USDT')).toBeTruthy(); expect(getByText('$27.02')).toBeTruthy(); }); - it('handles cancel button click', () => { const { getByText } = renderWithProvider(, { state: mockInitialState, }); const cancelButton = getByText('Cancel'); fireEvent.press(cancelButton); - expect(getByText('Are you sure you want to exit?')).toBeTruthy(); expect( getByText('Your search information will not be saved.'), diff --git a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap index 4d89a952349..3aef46b9799 100644 --- a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap +++ b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap @@ -87,6 +87,7 @@ exports[`ManageNetworks should render correctly 1`] = ` style={null} > { @@ -95,7 +101,7 @@ const styles = StyleSheet.create({ disabled: { opacity: 0.3, }, - leftButtonContainer: { + rightElementContainer: { marginRight: 12, flexDirection: 'row', alignItems: 'flex-end', @@ -113,16 +119,11 @@ const styles = StyleSheet.create({ metamaskNameWrapper: { marginLeft: Device.isAndroid() ? 20 : 0, }, - fox: { - width: 24, - height: 24, + leftElementContainer: { marginLeft: 16, }, notificationsWrapper: { - position: 'relative', - flex: 1, - justifyContent: 'center', - alignItems: 'center', + marginHorizontal: 4, }, notificationsBadge: { width: 8, @@ -133,6 +134,9 @@ const styles = StyleSheet.create({ top: 2, right: 10, }, + addressCopyWrapper: { + marginHorizontal: 4, + }, }); const metamask_name = require('../../../images/metamask-name.png'); // eslint-disable-line @@ -903,12 +907,28 @@ export function getOfflineModalNavbar() { } /** - * Function that returns the navigation options - * for our wallet screen, + * Function that returns the navigation options for the wallet screen. * - * @returns {Object} - Corresponding navbar options containing headerTitle, headerTitle and headerTitle + * @param {Object} accountActionsRef - The ref object for the account actions + * @param {string} selectedAddress - The currently selected Ethereum address + * @param {string} accountName - The name of the currently selected account + * @param {string} accountAvatarType - The type of avatar for the currently selected account + * @param {string} networkName - The name of the current network + * @param {Object} networkImageSource - The image source for the network icon + * @param {Function} onPressTitle - Callback function when the title is pressed + * @param {Object} navigation - The navigation object + * @param {Object} themeColors - The theme colors object + * @param {boolean} isNotificationEnabled - Whether notifications are enabled + * @param {boolean | null} isProfileSyncingEnabled - Whether profile syncing is enabled + * @param {number} unreadNotificationCount - The number of unread notifications + * @param {number} readNotificationCount - The number of read notifications + * @returns {Object} An object containing the navbar options for the wallet screen */ export function getWalletNavbarOptions( + accountActionsRef, + selectedAddress, + accountName, + accountAvatarType, networkName, networkImageSource, onPressTitle, @@ -921,7 +941,7 @@ export function getWalletNavbarOptions( ) { const innerStyles = StyleSheet.create({ headerStyle: { - backgroundColor: themeColors.background.default, + backgroundColor: themeColors.background, shadowColor: importedColors.transparent, elevation: 0, }, @@ -1005,24 +1025,40 @@ export function getWalletNavbarOptions( return { headerTitle: () => ( + { + navigation.navigate(...createAccountSelectorNavDetails({})); + }} + accountTypeLabel={getLabelTextByAddress(selectedAddress) || undefined} + showAddress + cellAccountContainerStyle={styles.account} + testID={WalletViewSelectorsIDs.ACCOUNT_ICON} + /> + + ), + headerLeft: () => ( + ), - headerLeft: () => ( - - ), headerRight: () => ( - + + + + {isNotificationsFeatureEnabled() && ( diff --git a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap index 8099909473b..fa6b7d2bf5b 100644 --- a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap @@ -233,6 +233,7 @@ exports[`NetworkDetails renders correctly 1`] = ` style={null} > { expect(toJSON()).toMatchSnapshot(); }); - it('shows the account address', () => { - const { getByTestId } = renderWithProvider(, { - state: mockInitialState, - }); - expect(getByTestId(WalletViewSelectorsIDs.ACCOUNT_ADDRESS)).toBeDefined(); - }); - it('copies the account address to the clipboard when the copy button is pressed', async () => { const { getByTestId } = renderWithProvider(, { state: mockInitialState, diff --git a/app/components/UI/WalletAccount/WalletAccount.tsx b/app/components/UI/WalletAccount/WalletAccount.tsx index 8aa6f499ea6..d42f757971b 100644 --- a/app/components/UI/WalletAccount/WalletAccount.tsx +++ b/app/components/UI/WalletAccount/WalletAccount.tsx @@ -96,7 +96,7 @@ const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => { /> - + - - - - - - + + + + + + + + + + - - - + } + testID="account-label" + > + Account 2 + + - - - Account 2 - - - - - + width={12} + /> + + - - Address - : - - - 0xC496...a756 - diff --git a/app/components/Views/AccountActions/AccountActions.test.tsx b/app/components/Views/AccountActions/AccountActions.test.tsx index 02655b77080..730761ea373 100644 --- a/app/components/Views/AccountActions/AccountActions.test.tsx +++ b/app/components/Views/AccountActions/AccountActions.test.tsx @@ -66,6 +66,18 @@ jest.mock('@react-navigation/native', () => { navigate: mockNavigate, goBack: mockGoBack, }), + useRoute: () => ({ + params: { + selectedAccount: { + address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', + metadata: { + keyring: { + type: 'HD Key Tree', + }, + }, + }, + }, + }), }; }); @@ -130,7 +142,7 @@ describe('AccountActions', () => { expect(mockNavigate).toHaveBeenCalledWith('Webview', { screen: 'SimpleWebview', params: { - url: 'https://etherscan.io/address/0xc4966c0d659d99699bfd7eb54d8fafee40e4a756', + url: 'https://etherscan.io/address/0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', title: 'etherscan.io', }, }); @@ -144,7 +156,7 @@ describe('AccountActions', () => { fireEvent.press(getByTestId(AccountActionsModalSelectorsIDs.SHARE_ADDRESS)); expect(Share.open).toHaveBeenCalledWith({ - message: '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756', + message: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', }); }); @@ -162,6 +174,14 @@ describe('AccountActions', () => { { credentialName: 'private_key', shouldUpdateNav: true, + selectedAccount: { + address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', + metadata: { + keyring: { + type: 'HD Key Tree', + }, + }, + }, }, ); }); @@ -173,7 +193,16 @@ describe('AccountActions', () => { fireEvent.press(getByTestId(AccountActionsModalSelectorsIDs.EDIT_ACCOUNT)); - expect(mockNavigate).toHaveBeenCalledWith('EditAccountName'); + expect(mockNavigate).toHaveBeenCalledWith('EditAccountName', { + selectedAccount: { + address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', + metadata: { + keyring: { + type: 'HD Key Tree', + }, + }, + }, + }); }); describe('clicks remove account', () => { diff --git a/app/components/Views/AccountActions/AccountActions.tsx b/app/components/Views/AccountActions/AccountActions.tsx index 99547d6cfda..c44a1bc31a1 100644 --- a/app/components/Views/AccountActions/AccountActions.tsx +++ b/app/components/Views/AccountActions/AccountActions.tsx @@ -1,11 +1,17 @@ // Third party dependencies. import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Alert, View, Text } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { + useNavigation, + RouteProp, + ParamListBase, + useRoute, +} from '@react-navigation/native'; import { useDispatch, useSelector } from 'react-redux'; import Share from 'react-native-share'; -// External dependencies. +// External dependencies +import { InternalAccount } from '@metamask/keyring-api'; import BottomSheet, { BottomSheetRef, } from '../../../component-library/components/BottomSheets/BottomSheet'; @@ -25,7 +31,6 @@ import { selectNetworkConfigurations, selectProviderConfig, } from '../../../selectors/networkController'; -import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; import { strings } from '../../../../locales/i18n'; // Internal dependencies import styleSheet from './AccountActions.styles'; @@ -50,7 +55,13 @@ import BlockingActionModal from '../../UI/BlockingActionModal'; import { useTheme } from '../../../util/theme'; import { Hex } from '@metamask/utils'; +interface AccountActionsParams { + selectedAccount: InternalAccount; +} + const AccountActions = () => { + const route = useRoute>(); + const { selectedAccount } = route.params as AccountActionsParams; const { colors } = useTheme(); const styles = styleSheet(colors); const sheetRef = useRef(null); @@ -67,7 +78,6 @@ const AccountActions = () => { const providerConfig = useSelector(selectProviderConfig); - const selectedAccount = useSelector(selectSelectedInternalAccount); const selectedAddress = selectedAccount?.address; const keyring = selectedAccount?.metadata.keyring; @@ -140,6 +150,7 @@ const AccountActions = () => { navigate(Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL, { credentialName: 'private_key', shouldUpdateNav: true, + selectedAccount, }); }); }; @@ -305,7 +316,7 @@ const AccountActions = () => { ]); const goToEditAccountName = () => { - navigate('EditAccountName'); + navigate('EditAccountName', { selectedAccount }); }; const isExplorerVisible = Boolean( diff --git a/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap b/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap index 226158d5611..8fbd535e84c 100644 --- a/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap +++ b/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap @@ -281,6 +281,7 @@ exports[`AccountPermissions renders correctly 1`] = ` style={null} > { const actualReactNavigation = jest.requireActual('@react-navigation/native'); return { @@ -69,6 +80,7 @@ jest.mock('@react-navigation/native', () => { setOptions: mockSetOptions, goBack: mockGoBack, }), + useRoute: () => mockRoute, }; }); diff --git a/app/components/Views/EditAccountName/EditAccountName.tsx b/app/components/Views/EditAccountName/EditAccountName.tsx index d6b4a043e95..2d6c0bacc22 100644 --- a/app/components/Views/EditAccountName/EditAccountName.tsx +++ b/app/components/Views/EditAccountName/EditAccountName.tsx @@ -1,10 +1,16 @@ // Third party dependencies import React, { useCallback, useEffect, useState } from 'react'; -import { useNavigation } from '@react-navigation/native'; +import { + useRoute, + useNavigation, + RouteProp, + ParamListBase, +} from '@react-navigation/native'; import { useSelector } from 'react-redux'; import { SafeAreaView } from 'react-native'; // External dependencies +import { InternalAccount } from '@metamask/keyring-api'; import Text from '../../../component-library/components/Texts/Text/Text'; import { View } from 'react-native-animatable'; import { TextVariant } from '../../../component-library/components/Texts/Text'; @@ -24,7 +30,6 @@ import { getEditAccountNameNavBarOptions } from '../../../components/UI/Navbar'; import Engine from '../../../core/Engine'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { selectChainId } from '../../../selectors/networkController'; -import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; import { doENSReverseLookup, isDefaultAccountName, @@ -37,7 +42,20 @@ import styleSheet from './EditAccountName.styles'; import { getDecimalChainId } from '../../../util/networks'; import { useMetrics } from '../../../components/hooks/useMetrics'; +interface RootNavigationParamList extends ParamListBase { + EditAccountName: { + selectedAccount: InternalAccount; + }; +} + +type EditAccountNameRouteProp = RouteProp< + RootNavigationParamList, + 'EditAccountName' +>; + const EditAccountName = () => { + const route = useRoute(); + const { selectedAccount } = route.params; const { colors } = useTheme(); const { trackEvent } = useMetrics(); const { styles } = useStyles(styleSheet, {}); @@ -45,10 +63,8 @@ const EditAccountName = () => { const [accountName, setAccountName] = useState(); const [ens, setEns] = useState(); - const selectedInternalAccount = useSelector(selectSelectedInternalAccount); - - const selectedChecksummedAddress = selectedInternalAccount?.address - ? toChecksumHexAddress(selectedInternalAccount.address) + const selectedChecksummedAddress = selectedAccount?.address + ? toChecksumHexAddress(selectedAccount.address) : undefined; const chainId = useSelector(selectChainId); @@ -80,28 +96,22 @@ const EditAccountName = () => { }, [updateNavBar]); useEffect(() => { - const name = selectedInternalAccount?.metadata.name; + const name = selectedAccount?.metadata.name; setAccountName(isDefaultAccountName(name) && ens ? ens : name); - }, [ens, selectedInternalAccount?.metadata.name]); + }, [ens, selectedAccount?.metadata.name]); const onChangeName = (name: string) => { setAccountName(name); }; const saveAccountName = async () => { - if ( - accountName && - accountName.length > 0 && - selectedInternalAccount?.address - ) { - Engine.setAccountLabel(selectedInternalAccount?.address, accountName); + if (accountName && accountName.length > 0 && selectedAccount?.address) { + Engine.setAccountLabel(selectedAccount?.address, accountName); navigate('WalletView'); try { const analyticsProperties = async () => { - const accountType = getAddressAccountType( - selectedInternalAccount?.address, - ); + const accountType = getAddressAccountType(selectedAccount?.address); const account_type = accountType === 'QR' ? 'hardware' : accountType; return { account_type, chain_id: getDecimalChainId(chainId) }; }; @@ -131,13 +141,10 @@ const EditAccountName = () => { {strings('address_book.address')} - {selectedInternalAccount?.address ? ( + {selectedAccount?.address ? ( ) : null} diff --git a/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap b/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap index 5de9ca67544..9e6a8d2bc18 100644 --- a/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap +++ b/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap @@ -87,7 +87,7 @@ exports[`EditAccountName should render correctly 1`] = ` } } testID="account-name-input" - value="Account 1" + value="Test Account" /> diff --git a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap index 04b33ca2b84..c2d68ea9cf5 100644 --- a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap @@ -185,6 +185,7 @@ exports[`OnboardingGeneralSettings should render correctly 1`] = ` style={null} > ; + interface IRevealPrivateCredentialProps { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any navigation: any; credentialName: string; cancel: () => void; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - route: any; + route: RevealPrivateCredentialRouteProp; } const RevealPrivateCredential = ({ @@ -91,9 +104,10 @@ const RevealPrivateCredential = ({ const [clipboardEnabled, setClipboardEnabled] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); - const selectedAddress = useSelector( + const checkSummedAddress = useSelector( selectSelectedInternalAccountChecksummedAddress, ); + // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const passwordSet = useSelector((state: any) => state.user.passwordSet); @@ -106,6 +120,8 @@ const RevealPrivateCredential = ({ const styles = createStyles(theme); const credentialSlug = credentialName || route?.params.credentialName; + const selectedAddress = + route?.params?.selectedAccount?.address || checkSummedAddress; const isPrivateKey = credentialSlug === PRIVATE_KEY; const updateNavBar = () => { diff --git a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap index 3ae0062fa7f..4a8e888f4a9 100644 --- a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap @@ -1266,3 +1266,376 @@ exports[`RevealPrivateCredential renders reveal private key correctly 1`] = ` `; + +exports[`RevealPrivateCredential renders with a custom selectedAddress 1`] = ` + + + + + + + Save it somewhere safe and secret. + + + + + + + Never disclose this key. Anyone with your private key can fully control your account, including transferring away any of your funds. + + + + + + Enter password to continue + + + + + + + + Cancel + + + + + Next + + + + + + + + + +`; diff --git a/app/components/Views/RevealPrivateCredential/index.test.tsx b/app/components/Views/RevealPrivateCredential/index.test.tsx index 9057bf751d9..6e9d90b7916 100644 --- a/app/components/Views/RevealPrivateCredential/index.test.tsx +++ b/app/components/Views/RevealPrivateCredential/index.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +import { InternalAccount } from '@metamask/keyring-api'; import { backgroundState } from '../../../util/test/initial-root-state'; import { RevealPrivateCredential } from './'; import { ThemeContext, mockTheme } from '../../../util/theme'; @@ -43,8 +44,10 @@ describe('RevealPrivateCredential', () => { const { toJSON } = renderWithProviders( { const { toJSON } = renderWithProviders( { const { toJSON } = renderWithProviders( { const { getByPlaceholderText, getByTestId } = renderWithProviders( { const { getByPlaceholderText, getByTestId } = renderWithProviders( { ).toBeTruthy(); }); }); + + it('renders with a custom selectedAddress', async () => { + const mockInternalAccount: InternalAccount = { + type: 'eip155:eoa', + id: 'unique-account-id-1', + address: '0x1234567890123456789012345678901234567890', + options: { + someOption: 'optionValue', + anotherOption: 42, + }, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_sendTransaction', + ], + metadata: { + name: 'Test Account', + importTime: Date.now(), + keyring: { + type: 'HD Key Tree', + }, + nameLastUpdatedAt: Date.now(), + snap: { + id: 'npm:@metamask/test-snap', + name: 'Test Snap', + enabled: true, + }, + lastSelected: Date.now(), + }, + }; + + const { toJSON } = renderWithProviders( + null} + credentialName={PRIV_KEY_CREDENTIAL} + />, + ); + expect(toJSON()).toMatchSnapshot(); + }); }); diff --git a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap index 7822a45d4f8..9514928b1a6 100644 --- a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap @@ -54,7 +54,16 @@ exports[`Wallet should render correctly 1`] = ` collapsable={false} style={ { - "backgroundColor": "#ffffff", + "backgroundColor": { + "alternative": "#f2f4f6", + "alternativeHover": "#e7ebee", + "alternativePressed": "#dbe0e6", + "default": "#ffffff", + "defaultHover": "#f5f5f5", + "defaultPressed": "#ebebeb", + "hover": "#0000000a", + "pressed": "#00000014", + }, "borderBottomColor": "rgb(216, 216, 216)", "elevation": 0, "flex": 1, @@ -115,42 +124,11 @@ exports[`Wallet should render correctly 1`] = ` "top": 0, } } - > - - - @@ -170,9 +148,14 @@ exports[`Wallet should render correctly 1`] = ` testID="open-networks-button" > - - Ethereum Mainnet - + + + + + + + + + + + + + + + + + + + + Account 2 + + + + + + + + 0xC496...a756 + + + + + + + + + + + + + @@ -294,9 +567,9 @@ exports[`Wallet should render correctly 1`] = ` testID="wallet-scan-button" > - - - - - - - An error occurred - - - Your information can't be shown. Don’t worry, your wallet and funds are safe. - - + } + > - - View: Wallet -TypeError: Cannot read properties of undefined (reading 'internalAccounts') - - - - - - -  - - - Try again - - - - - - - Please report this issue so we can fix it: - - + width={24} + /> + - -  - - - Take a screenshot of this screen. - - - -  - - - - Copy - - - the error message to clipboard. + Basic functionality is off - -  - - - Submit a ticket - - - here. - - - Please include the error message and the screenshot. - - - -  - - - Send us a bug report - - - here. - - - Please include details about what happened. - - - - If this error persists, - - - save your Secret Recovery Phrase + Turn on basic functionality - - & re-install the app. Note: you can NOT restore your wallet without your Secret Recovery Phrase. - + - - + + diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx index 87ef6fe0502..a7baa2653b3 100644 --- a/app/components/Views/Wallet/index.test.tsx +++ b/app/components/Views/Wallet/index.test.tsx @@ -5,59 +5,67 @@ import { act, screen } from '@testing-library/react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import Routes from '../../../constants/navigation/Routes'; import { backgroundState } from '../../../util/test/initial-root-state'; -import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; +import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; -import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors'; import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; import { AppState } from 'react-native'; const MOCK_ADDRESS = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272'; -const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ - MOCK_ADDRESS, -]); - -jest.mock('../../../core/Engine', () => ({ - getTotalFiatAccountBalance: jest.fn(), - context: { - NftController: { - allNfts: { - [MOCK_ADDRESS]: { - [MOCK_ADDRESS]: [], +jest.mock('../../../util/address', () => { + const actual = jest.requireActual('../../../util/address'); + return { + ...actual, + getLabelTextByAddress: jest.fn(), + }; +}); + +jest.mock('../../../core/Engine', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + getTotalFiatAccountBalance: jest.fn(), + context: { + NftController: { + allNfts: { + [MOCK_ADDRESS]: { + [MOCK_ADDRESS]: [], + }, }, + allNftContracts: { + [MOCK_ADDRESS]: { + [MOCK_ADDRESS]: [], + }, + }, + }, + TokenRatesController: { + poll: jest.fn(), + }, + TokenDetectionController: { + detectTokens: jest.fn(), }, - allNftContracts: { - [MOCK_ADDRESS]: { - [MOCK_ADDRESS]: [], + NftDetectionController: { + detectNfts: jest.fn(), + }, + AccountTrackerController: { + refresh: jest.fn(), + }, + KeyringController: { + state: { + keyrings: [ + { + accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'], + }, + ], }, }, - }, - TokenRatesController: { - poll: jest.fn(), - }, - TokenDetectionController: { - detectTokens: jest.fn(), - }, - NftDetectionController: { - detectNfts: jest.fn(), - }, - AccountTrackerController: { - refresh: jest.fn(), - }, - KeyringController: { - state: { - keyrings: [ - { - accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'], - }, - ], + AccountsController: { + ...mockAccountsControllerState, + state: mockAccountsControllerState, }, }, - AccountsController: { - ...MOCK_ACCOUNTS_CONTROLLER_STATE, - }, - }, -})); + }; +}); const mockInitialState = { networkOnboarded: { @@ -117,6 +125,21 @@ jest.mock('../../../util/notifications/hooks/useAccountSyncing', () => ({ }), })); +jest.mock('../../../util/address', () => ({ + ...jest.requireActual('../../../util/address'), + getInternalAccountByAddress: jest.fn().mockReturnValue({ + address: MOCK_ADDRESS, + balance: '0x0', + name: 'Account 1', + type: 'default', + metadata: { + keyring: { + type: 'HD Key Tree', + }, + }, + }), +})); + const render = (Component: React.ComponentType) => renderScreen( Component, @@ -147,11 +170,21 @@ describe('Wallet', () => { render(Wallet); expect(ScrollableTabView).toHaveBeenCalled(); }); - it('should render fox icon', () => { + it('should render the address copy button', () => { + //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar + render(Wallet); + const addressCopyButton = screen.getByTestId( + WalletViewSelectorsIDs.NAVBAR_ADDRESS_COPY_BUTTON, + ); + expect(addressCopyButton).toBeDefined(); + }); + it('should render the account picker', () => { //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar render(Wallet); - const foxIcon = screen.getByTestId(CommonSelectorsIDs.FOX_ICON); - expect(foxIcon).toBeDefined(); + const accountPicker = screen.getByTestId( + WalletViewSelectorsIDs.ACCOUNT_ICON, + ); + expect(accountPicker).toBeDefined(); }); it('dispatches account syncing on mount', () => { jest.clearAllMocks(); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 5f04c86d930..864beb3876d 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -35,6 +35,7 @@ import { ToastContext, ToastVariants, } from '../../../component-library/components/Toast'; +import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; import NotificationsService from '../../../util/notifications/services/NotificationService'; import Engine from '../../../core/Engine'; import CollectibleContracts from '../../UI/CollectibleContracts'; @@ -66,7 +67,6 @@ import { ParamListBase, useNavigation, } from '@react-navigation/native'; -import { WalletAccount } from '../../../components/UI/WalletAccount'; import { selectConversionRate, selectCurrentCurrency, @@ -94,6 +94,7 @@ import { } from '../../../selectors/notifications'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; +import { useAccountName } from '../../hooks/useAccountName'; import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance'; @@ -230,6 +231,14 @@ const Wallet = ({ const currentToast = toastRef?.current; + const accountName = useAccountName(); + + const accountAvatarType = useSelector((state: RootState) => + state.settings.useBlockieIcon + ? AvatarAccountType.Blockies + : AvatarAccountType.JazzIcon, + ); + useEffect(() => { if ( isDataCollectionForMarketingEnabled === null && @@ -438,6 +447,10 @@ const Wallet = ({ useEffect(() => { navigation.setOptions( getWalletNavbarOptions( + walletRef, + selectedAddress || '', + accountName, + accountAvatarType, networkName, networkImageSource, onTitlePress, @@ -451,6 +464,9 @@ const Wallet = ({ ); /* eslint-disable-next-line */ }, [ + selectedAddress, + accountName, + accountAvatarType, navigation, colors, networkName, @@ -560,9 +576,6 @@ const Wallet = ({ /> ) : null} - {selectedAddress ? ( - - ) : null} <> {accountBalanceByChainId && } ({ + useSelector: jest.fn(), +})); +jest.mock('../../components/hooks/useEnsNameByAddress', () => jest.fn()); +jest.mock('../../util/ENSUtils', () => ({ + isDefaultAccountName: jest.fn(), +})); + +describe('useAccountName', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the ENS name when default name is a default account name', () => { + (useSelector as jest.Mock).mockReturnValue({ + metadata: { name: 'Account 1' }, + address: '0x1234567890123456789012345678901234567890', + }); + (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' }); + (isDefaultAccountName as jest.Mock).mockReturnValue(true); + + const { result } = renderHook(() => useAccountName()); + expect(result.current).toBe('test.eth'); + }); + + it('should return the default name when it is not a default account name', () => { + (useSelector as jest.Mock).mockReturnValue({ + metadata: { name: 'My Custom Account' }, + address: '0x1234567890123456789012345678901234567890', + }); + (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' }); + (isDefaultAccountName as jest.Mock).mockReturnValue(false); + + const { result } = renderHook(() => useAccountName()); + expect(result.current).toBe('My Custom Account'); + }); + + it('should return an empty string when both default name and ENS name are undefined', () => { + (useSelector as jest.Mock).mockReturnValue({ + metadata: { name: undefined }, + address: '0x1234567890123456789012345678901234567890', + }); + (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: undefined }); + (isDefaultAccountName as jest.Mock).mockReturnValue(false); + + const { result } = renderHook(() => useAccountName()); + expect(result.current).toBe(''); + }); + + it('should return an empty string when default name is undefined and ENS name is available', () => { + (useSelector as jest.Mock).mockReturnValue({ + metadata: { name: undefined }, + address: '0x1234567890123456789012345678901234567890', + }); + (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' }); + (isDefaultAccountName as jest.Mock).mockReturnValue(false); + + const { result } = renderHook(() => useAccountName()); + expect(result.current).toBe(''); + }); + + it('should return the ENS name when default name is a default account name and ENS name is available', () => { + (useSelector as jest.Mock).mockReturnValue({ + metadata: { name: 'Account 1' }, + address: '0x1234567890123456789012345678901234567890', + }); + (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' }); + (isDefaultAccountName as jest.Mock).mockReturnValue(true); + + const { result } = renderHook(() => useAccountName()); + expect(result.current).toBe('test.eth'); + }); +}); diff --git a/app/components/hooks/useAccountName.ts b/app/components/hooks/useAccountName.ts new file mode 100644 index 00000000000..72ec9e8dd53 --- /dev/null +++ b/app/components/hooks/useAccountName.ts @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { selectSelectedInternalAccount } from '../../selectors/accountsController'; +import useEnsNameByAddress from '../../components/hooks/useEnsNameByAddress'; +import { isDefaultAccountName } from '../../util/ENSUtils'; + +export const useAccountName = () => { + const selectedAccount = useSelector(selectSelectedInternalAccount); + const { ensName } = useEnsNameByAddress(selectedAccount?.address); + const defaultName = selectedAccount?.metadata?.name; + + return useMemo( + () => + (isDefaultAccountName(defaultName) && ensName ? ensName : defaultName) || + '', + [defaultName, ensName], + ); +}; diff --git a/e2e/pages/AccountListView.js b/e2e/pages/AccountListView.js index 9bcc8daa11a..4665817b0b4 100644 --- a/e2e/pages/AccountListView.js +++ b/e2e/pages/AccountListView.js @@ -3,6 +3,7 @@ import { AccountListViewSelectorsIDs, AccountListViewSelectorsText, } from '../selectors/AccountListView.selectors'; +import { WalletViewSelectorsIDs } from '../selectors/wallet/WalletView.selectors'; import { ConnectAccountBottomSheetSelectorsIDs } from '../selectors/Browser/ConnectAccountBottomSheet.selectors'; import Matchers from '../utils/Matchers'; import Gestures from '../utils/Gestures'; @@ -19,6 +20,10 @@ class AccountListView { ); } + get accountTagLabel() { + return Matchers.getElementByID(CellModalSelectorsIDs.TAG_LABEL); + } + get title() { return Matchers.getElementByText( AccountListViewSelectorsText.ACCOUNTS_LIST_TITLE, @@ -57,10 +62,25 @@ class AccountListView { getSelectElement(index) { return Matchers.getElementByID(CellModalSelectorsIDs.SELECT, index); } + getMultiselectElement(index) { return Matchers.getElementByID(CellModalSelectorsIDs.MULTISELECT, index); } + getSelectWithMenuElement(index) { + return Matchers.getElementByID( + CellModalSelectorsIDs.SELECT_WITH_MENU, + index, + ); + } + + async tapEditAccountActionsAtIndex(index) { + const accountActionsButton = Matchers.getElementByID( + `${WalletViewSelectorsIDs.ACCOUNT_ACTIONS}-${index}`, + ); + await Gestures.waitAndTap(accountActionsButton); + } + async accountNameInList(accountName) { return Matchers.getElementByText(accountName, 1); } @@ -69,7 +89,11 @@ class AccountListView { } async tapToSelectActiveAccountAtIndex(index) { - await Gestures.tap(this.getSelectElement(index)); + await Gestures.tap(this.getSelectWithMenuElement(index)); + } + + async longPressAccountAtIndex(index) { + await Gestures.tapAndLongPress(this.getSelectWithMenuElement(index)); } async tapAddAccountButton() { diff --git a/e2e/pages/wallet/WalletView.js b/e2e/pages/wallet/WalletView.js index 7443eaf2c67..1f054e25ec2 100644 --- a/e2e/pages/wallet/WalletView.js +++ b/e2e/pages/wallet/WalletView.js @@ -2,7 +2,6 @@ import { WalletViewSelectorsIDs, WalletViewSelectorsText, } from '../../selectors/wallet/WalletView.selectors'; -import { CommonSelectorsText } from '../../selectors/Common.selectors'; import Gestures from '../../utils/Gestures'; import Matchers from '../../utils/Matchers'; @@ -35,6 +34,12 @@ class WalletView { ); } + async getNavbarNetworkPicker() { + return Matchers.getElementByID( + WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER, + ); + } + get nftTab() { return Matchers.getElementByText(WalletViewSelectorsText.NFTS_TAB); } @@ -75,12 +80,14 @@ class WalletView { return Matchers.getElementByText(WalletViewSelectorsText.HIDE_TOKENS); } - get mainWalletAccountActions() { - return Matchers.getElementByID(WalletViewSelectorsIDs.ACCOUNT_ACTIONS); + get currentMainWalletAccountActions() { + return Matchers.getElementByID( + WalletViewSelectorsIDs.ACCOUNT_NAME_LABEL_TEXT, + ); } - async tapMainWalletAccountActions() { - await Gestures.waitAndTap(this.mainWalletAccountActions); + async tapCurrentMainWalletAccountActions() { + await Gestures.waitAndTap(this.currentMainWalletAccountActions); } async tapOnToken(token) { diff --git a/e2e/selectors/wallet/WalletView.selectors.js b/e2e/selectors/wallet/WalletView.selectors.js index 265a13cd0b1..5b02e02cf55 100644 --- a/e2e/selectors/wallet/WalletView.selectors.js +++ b/e2e/selectors/wallet/WalletView.selectors.js @@ -23,13 +23,14 @@ export const WalletViewSelectorsIDs = { ACCOUNT_OVERVIEW: 'account-overview', ACCOUNT_ACTIONS: 'main-wallet-account-actions', ACCOUNT_COPY_BUTTON: 'wallet-account-copy-button', - ACCOUNT_ADDRESS: 'wallet-account-address', TEST_COLLECTIBLE: 'collectible-Test Dapp NFTs #1-1', COLLECTIBLE_FALLBACK: 'fallback-nft-with-token-id', + NAVBAR_ADDRESS_COPY_BUTTON: 'navbar-address-copy-button', IMPORT_TOKEN_FOOTER_LINK: 'import-token-footer-link', SORT_DECLINING_BALANCE: 'sort-declining-balance', SORT_ALPHABETICAL: 'sort-alphabetical', SORT_BY: 'sort-by', + NAVBAR_NETWORK_PICKER: 'network-avatar-picker', }; export const WalletViewSelectorsText = { diff --git a/e2e/specs/accounts/change-account-name.spec.js b/e2e/specs/accounts/change-account-name.spec.js index f20c6ee3356..229c201d8cc 100644 --- a/e2e/specs/accounts/change-account-name.spec.js +++ b/e2e/specs/accounts/change-account-name.spec.js @@ -20,14 +20,21 @@ import Assertions from '../../utils/Assertions'; import TabBarComponent from '../../pages/TabBarComponent'; import SettingsView from '../../pages/Settings/SettingsView'; import LoginView from '../../pages/LoginView'; +import AccountListView from '../../pages/AccountListView'; const fixtureServer = new FixtureServer(); const NEW_ACCOUNT_NAME = 'Edited Name'; +const NEW_IMPORTED_ACCOUNT_NAME = 'New Imported Account'; +const MAIN_ACCOUNT_INDEX = 0; +const IMPORTED_ACCOUNT_INDEX = 1; describe(Regression('Change Account Name'), () => { beforeAll(async () => { await TestHelpers.reverseServerPort(); - const fixture = new FixtureBuilder().withGanacheNetwork().build(); + const fixture = new FixtureBuilder() + .withGanacheNetwork() + .withImportedAccountKeyringController() + .build(); await startFixtureServer(fixtureServer); await loadFixture(fixtureServer, { fixture }); await device.launchApp({ @@ -43,7 +50,9 @@ describe(Regression('Change Account Name'), () => { it('renames an account and verifies the new name persists after locking and unlocking the wallet', async () => { // Open account actions and edit account name - await WalletView.tapMainWalletAccountActions(); + await TabBarComponent.tapWallet(); + await WalletView.tapIdenticon(); + await AccountListView.tapEditAccountActionsAtIndex(MAIN_ACCOUNT_INDEX); await AccountActionsModal.tapEditAccount(); await Gestures.clearField(EditAccountNameView.accountNameInput); await TestHelpers.typeTextAndHideKeyboard( @@ -73,4 +82,44 @@ describe(Regression('Change Account Name'), () => { NEW_ACCOUNT_NAME, ); }); + + it('import an account, edits the name, and verifies the new name persists after locking and unlocking the wallet', async () => { + // Open account actions bottom sheet and choose imported account + await WalletView.tapIdenticon(); + await AccountListView.tapToSelectActiveAccountAtIndex( + IMPORTED_ACCOUNT_INDEX, + ); + + // Edit imported account name + await WalletView.tapIdenticon(); + await AccountListView.tapEditAccountActionsAtIndex(IMPORTED_ACCOUNT_INDEX); + await AccountActionsModal.tapEditAccount(); + await Gestures.clearField(EditAccountNameView.accountNameInput); + await TestHelpers.typeTextAndHideKeyboard( + EditAccountNameSelectorIDs.ACCOUNT_NAME_INPUT, + NEW_IMPORTED_ACCOUNT_NAME, + ); + await EditAccountNameView.tapSave(); + + // Verify updated name + await Assertions.checkIfElementToHaveText( + WalletView.accountName, + NEW_IMPORTED_ACCOUNT_NAME, + ); + + // Lock wallet + await Assertions.checkIfVisible(TabBarComponent.tabBarSettingButton); + await TabBarComponent.tapSettings(); + await SettingsView.scrollToLockButton(); + await SettingsView.tapLock(); + await SettingsView.tapYesAlertButton(); + await Assertions.checkIfVisible(LoginView.container); + + // Unlock wallet and verify updated name persists + await loginToApp(); + await Assertions.checkIfElementToHaveText( + WalletView.accountName, + NEW_IMPORTED_ACCOUNT_NAME, + ); + }); }); diff --git a/e2e/specs/accounts/imported-account-remove-and-import.spec.js b/e2e/specs/accounts/imported-account-remove-and-import.spec.js index 84847df1141..6dccfbf6032 100644 --- a/e2e/specs/accounts/imported-account-remove-and-import.spec.js +++ b/e2e/specs/accounts/imported-account-remove-and-import.spec.js @@ -24,6 +24,7 @@ const fixtureServer = new FixtureServer(); // It should NEVER hold any eth or token const TEST_PRIVATE_KEY = 'cbfd798afcfd1fd8ecc48cbecb6dc7e876543395640b758a90e11d986e758ad1'; +const ACCOUNT_INDEX = 1; describe( Regression('removes and reimports an account using a private key'), @@ -50,7 +51,7 @@ describe( await WalletView.tapIdenticon(); // Remove the imported account - await AccountListView.longPressImportedAccount(); + await AccountListView.longPressAccountAtIndex(ACCOUNT_INDEX); await AccountListView.tapYesToRemoveImportedAccountAlertButton(); await Assertions.checkIfNotVisible(AccountListView.accountTypeLabel); @@ -61,8 +62,14 @@ describe( await ImportAccountView.enterPrivateKey(TEST_PRIVATE_KEY); await Assertions.checkIfVisible(SuccessImportAccountView.container); await SuccessImportAccountView.tapCloseButton(); - await Assertions.checkIfElementToHaveText( - AccountListView.accountTypeLabel, + + const tagElement = await AccountListView.accountTagLabel; + const tagElementAttribute = await tagElement.getAttributes(); + const tagLabel = tagElementAttribute.label; + + // Check if the account type label is visible + await Assertions.checkIfTextMatches( + tagLabel, AccountListViewSelectorsText.ACCOUNT_TYPE_LABEL_TEXT, ); }); diff --git a/e2e/specs/accounts/reveal-private-key.spec.js b/e2e/specs/accounts/reveal-private-key.spec.js index 42e1dfe1c6a..790f1b9098b 100644 --- a/e2e/specs/accounts/reveal-private-key.spec.js +++ b/e2e/specs/accounts/reveal-private-key.spec.js @@ -27,7 +27,8 @@ const HD_ACCOUNT_1_PRIVATE_KEY = '242251a690016cfcf8af43fb1ad7ff4c66c269bbca03f9f076ee8db93c191594'; const IMPORTED_ACCOUNT_2_PRIVATE_KEY = 'cbfd798afcfd1fd8ecc48cbecb6dc7e876543395640b758a90e11d986e758ad1'; -const IMPORTED_ACCOUNT_2_INDEX = 1; +const IMPORTED_ACCOUNT_0_INDEX = 0; +const IMPORTED_ACCOUNT_1_INDEX = 1; describe(Regression('reveal private key'), () => { const PASSWORD = '123123123'; @@ -85,15 +86,43 @@ describe(Regression('reveal private key'), () => { ); }); - it('reveals the correct private key for an imported account from the account menu ', async () => { + it('reveals the correct private key for the first account in the account list ', async () => { await TabBarComponent.tapWallet(); await WalletView.tapIdenticon(); - await AccountListView.tapToSelectActiveAccountAtIndex( - IMPORTED_ACCOUNT_2_INDEX, + await AccountListView.tapEditAccountActionsAtIndex( + IMPORTED_ACCOUNT_0_INDEX, ); + + await AccountActionsModal.tapShowPrivateKey(); + await RevealPrivateKey.enterPasswordToRevealSecretCredential(PASSWORD); + await RevealPrivateKey.tapToReveal(); + await Assertions.checkIfVisible(RevealPrivateKey.container); + await Assertions.checkIfTextIsDisplayed( + RevealSeedViewSelectorsText.REVEAL_CREDENTIAL_PRIVATE_KEY_TITLE_TEXT, + ); + await Assertions.checkIfTextIsDisplayed(HD_ACCOUNT_1_PRIVATE_KEY); + + // Copy to clipboard + // Android devices running OS version < 11 (API level 29) will not see the copy to clipboard button presented + // This will cause the following step to fail if e2e were being run on an older android OS prior to our minimum API level 29 + // See details here: https://github.com/MetaMask/metamask-mobile/pull/4170 + await RevealPrivateKey.tapToCopyCredentialToClipboard(); + await RevealPrivateKey.tapToRevealPrivateCredentialQRCode(); + await Assertions.checkIfVisible( + RevealPrivateKey.revealCredentialQRCodeImage, + ); + await RevealPrivateKey.scrollToDone(); + await RevealPrivateKey.tapDoneButton(); await Assertions.checkIfVisible(WalletView.container); + }); + + it('reveals the correct private key for the second account in the account list which is also an imported account', async () => { + await TabBarComponent.tapWallet(); + await WalletView.tapIdenticon(); + await AccountListView.tapEditAccountActionsAtIndex( + IMPORTED_ACCOUNT_1_INDEX, + ); - await WalletView.tapMainWalletAccountActions(); await AccountActionsModal.tapShowPrivateKey(); await RevealPrivateKey.enterPasswordToRevealSecretCredential(PASSWORD); await RevealPrivateKey.tapToReveal(); diff --git a/e2e/specs/networks/add-custom-rpc.spec.js b/e2e/specs/networks/add-custom-rpc.spec.js index a780f52d3ae..f9cec1f88ac 100644 --- a/e2e/specs/networks/add-custom-rpc.spec.js +++ b/e2e/specs/networks/add-custom-rpc.spec.js @@ -102,8 +102,9 @@ describe(Regression('Custom RPC Tests'), () => { await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); await Assertions.checkIfVisible(WalletView.container); - await Assertions.checkIfElementToHaveText( - WalletView.navbarNetworkText, + const networkPicker = await WalletView.getNavbarNetworkPicker(); + await Assertions.checkIfElementHasLabel( + networkPicker, CustomNetworks.Gnosis.providerConfig.nickname, ); }); @@ -113,8 +114,9 @@ describe(Regression('Custom RPC Tests'), () => { await WalletView.tapNetworksButtonOnNavBar(); await Assertions.checkIfVisible(NetworkListModal.networkScroll); - await Assertions.checkIfElementToHaveText( - WalletView.navbarNetworkText, + const networkPicker = await WalletView.getNavbarNetworkPicker(); + await Assertions.checkIfElementHasLabel( + networkPicker, CustomNetworks.Gnosis.providerConfig.nickname, ); }); @@ -126,22 +128,28 @@ describe(Regression('Custom RPC Tests'), () => { CustomNetworks.Sepolia.providerConfig.nickname, ); await Assertions.checkIfVisible(NetworkEducationModal.container); - await Assertions.checkIfElementToHaveText( - NetworkEducationModal.networkName, - CustomNetworks.Sepolia.providerConfig.nickname, - ); + await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); await Assertions.checkIfVisible(WalletView.container); + const networkPicker = await WalletView.getNavbarNetworkPicker(); + + await Assertions.checkIfElementHasLabel( + networkPicker, + CustomNetworks.Sepolia.providerConfig.nickname, + ); }); it('should switch back to Gnosis', async () => { await WalletView.tapNetworksButtonOnNavBar(); await NetworkListModal.scrollToBottomOfNetworkList(); - await Assertions.checkIfElementToHaveText( - WalletView.navbarNetworkText, + + const networkPicker = await WalletView.getNavbarNetworkPicker(); + await Assertions.checkIfElementHasLabel( + networkPicker, CustomNetworks.Sepolia.providerConfig.nickname, ); + await Assertions.checkIfVisible(NetworkListModal.networkScroll); await NetworkListModal.scrollToTopOfNetworkList(); // Change to back to Gnosis Network @@ -149,8 +157,8 @@ describe(Regression('Custom RPC Tests'), () => { CustomNetworks.Gnosis.providerConfig.nickname, ); await Assertions.checkIfVisible(WalletView.container); - await Assertions.checkIfElementToHaveText( - WalletView.navbarNetworkText, + await Assertions.checkIfElementHasLabel( + networkPicker, CustomNetworks.Gnosis.providerConfig.nickname, ); await Assertions.checkIfNotVisible(NetworkEducationModal.container); diff --git a/e2e/specs/networks/connect-test-network.spec.js b/e2e/specs/networks/connect-test-network.spec.js index 88710a09a92..71680d2cbb8 100644 --- a/e2e/specs/networks/connect-test-network.spec.js +++ b/e2e/specs/networks/connect-test-network.spec.js @@ -55,8 +55,10 @@ describe(Regression('Connect to a Test Network'), () => { await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); await Assertions.checkIfVisible(WalletView.container); - await Assertions.checkIfElementToHaveText( - WalletView.navbarNetworkText, + + const networkPicker = await WalletView.getNavbarNetworkPicker(); + await Assertions.checkIfElementHasLabel( + networkPicker, CustomNetworks.Sepolia.providerConfig.nickname, ); }); @@ -80,10 +82,9 @@ describe(Regression('Connect to a Test Network'), () => { await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); await Assertions.checkIfVisible(WalletView.container); - await Assertions.checkIfElementToHaveText( - WalletView.navbarNetworkText, - ETHEREUM, - ); + + const networkPicker = await WalletView.getNavbarNetworkPicker(); + await Assertions.checkIfElementHasLabel(networkPicker, ETHEREUM); }); it('should toggle off the Test Network switch', async () => { diff --git a/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js b/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js index 5d2f3cb2b16..30cadefba5e 100644 --- a/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js +++ b/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js @@ -63,6 +63,7 @@ describe(SmokeNotifications('Account syncing'), () => { ); await WalletView.tapIdenticon(); + await Assertions.checkIfVisible(AccountListView.accountList); for (const accountName of decryptedAccountNames) { @@ -74,8 +75,10 @@ describe(SmokeNotifications('Account syncing'), () => { await AccountListView.tapAddAccountButton(); await AddAccountModal.tapCreateAccount(); await AccountListView.swipeToDismissAccountsModal(); + await TestHelpers.delay(2000); + await WalletView.tapCurrentMainWalletAccountActions(); - await WalletView.tapMainWalletAccountActions(); + await AccountListView.tapEditAccountActionsAtIndex(2); await AccountActionsModal.renameActiveAccount(NEW_ACCOUNT_NAME); await Assertions.checkIfElementToHaveText( diff --git a/e2e/utils/Assertions.js b/e2e/utils/Assertions.js index e3b15781d21..ea2935fad2c 100644 --- a/e2e/utils/Assertions.js +++ b/e2e/utils/Assertions.js @@ -129,6 +129,27 @@ class Assertions { static async checkIfToggleIsOff(elementID) { return expect(await elementID).toHaveToggleValue(false); } + + /** + * Check if two text values match exactly. + * @param {string} actualText - The actual text value to check. + * @param {string} expectedText - The expected text value to match against. + */ + static async checkIfTextMatches(actualText, expectedText) { + try { + if (!actualText || !expectedText) { + throw new Error('Both actual and expected text must be provided'); + } + + return expect(actualText).toBe(expectedText); + } catch (error) { + if (actualText !== expectedText) { + throw new Error( + `Text matching failed.\nExpected: "${expectedText}"\nActual: "${actualText}"`, + ); + } + } + } } export default Assertions;