From 15036681b3078120a6067a8e82f80698e8d45856 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 19 Nov 2025 17:12:29 +0100 Subject: [PATCH 01/11] feat: remove multichain accounts state1 UI --- .../multichain-accounts-tree.tsx | 13 - .../account-details/account-details.tsx | 7 +- .../menu-items/account-details-menu-item.js | 37 +- ui/helpers/constants/routes.ts | 18 - .../confirmations/context/confirm/index.tsx | 15 - ui/pages/home/home.component.js | 14 +- ui/pages/home/home.container.js | 3 - .../account-type-utils.test.ts | 130 ----- .../account-details/account-type-utils.ts | 149 ----- .../account-details/btc-account-details.tsx | 15 - .../account-details/evm-account-details.tsx | 31 -- .../hardware-account-details.tsx | 24 - .../account-details/index.ts | 8 - .../institutional-evm-account-details.tsx | 15 - .../multichain-account-details.test.tsx | 167 ------ .../multichain-account-details.tsx | 77 --- .../private-key-account-details.tsx | 29 - .../solana-account-details.tsx | 23 - .../account-details/tron-account-details.tsx | 23 - .../address-qr-code/address-qr-code.scss | 3 - .../address-qr-code.stories.tsx | 221 -------- .../address-qr-code/address-qr-code.test.tsx | 191 ------- .../address-qr-code/address-qr-code.tsx | 123 ----- .../address-qr-code/index.ts | 1 - .../base-account-details.scss | 20 - .../base-account-details.stories.tsx | 220 -------- .../base-account-details.test.tsx | 516 ------------------ .../base-account-details.tsx | 263 --------- .../base-account-details/index.ts | 1 - .../multichain-account-details-page.tsx | 6 +- .../wallet-details/index.ts | 3 - .../wallet-details.component.stories.tsx | 58 -- .../wallet-details.component.test.tsx | 488 ----------------- .../wallet-details.component.tsx | 472 ---------------- .../wallet-details/wallet-details.scss | 27 - ui/pages/routes/routes.component.tsx | 57 +- ui/pages/routes/utils.js | 35 -- ui/store/actions.ts | 7 - 38 files changed, 12 insertions(+), 3498 deletions(-) delete mode 100644 ui/pages/multichain-accounts/account-details/account-type-utils.test.ts delete mode 100644 ui/pages/multichain-accounts/account-details/account-type-utils.ts delete mode 100644 ui/pages/multichain-accounts/account-details/btc-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/evm-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/hardware-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/index.ts delete mode 100644 ui/pages/multichain-accounts/account-details/institutional-evm-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/multichain-account-details.test.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/multichain-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/private-key-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/solana-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/account-details/tron-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/address-qr-code/address-qr-code.scss delete mode 100644 ui/pages/multichain-accounts/address-qr-code/address-qr-code.stories.tsx delete mode 100644 ui/pages/multichain-accounts/address-qr-code/address-qr-code.test.tsx delete mode 100644 ui/pages/multichain-accounts/address-qr-code/address-qr-code.tsx delete mode 100644 ui/pages/multichain-accounts/address-qr-code/index.ts delete mode 100644 ui/pages/multichain-accounts/base-account-details/base-account-details.scss delete mode 100644 ui/pages/multichain-accounts/base-account-details/base-account-details.stories.tsx delete mode 100644 ui/pages/multichain-accounts/base-account-details/base-account-details.test.tsx delete mode 100644 ui/pages/multichain-accounts/base-account-details/base-account-details.tsx delete mode 100644 ui/pages/multichain-accounts/base-account-details/index.ts delete mode 100644 ui/pages/multichain-accounts/wallet-details/index.ts delete mode 100644 ui/pages/multichain-accounts/wallet-details/wallet-details.component.stories.tsx delete mode 100644 ui/pages/multichain-accounts/wallet-details/wallet-details.component.test.tsx delete mode 100644 ui/pages/multichain-accounts/wallet-details/wallet-details.component.tsx delete mode 100644 ui/pages/multichain-accounts/wallet-details/wallet-details.scss diff --git a/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx b/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx index 8b5d10173485..6b77e9b04ba9 100644 --- a/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx +++ b/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx @@ -17,7 +17,6 @@ import { import { ConsolidatedWallets } from '../../../selectors/multichain-accounts/account-tree.types'; import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { HiddenAccountList } from '../../multichain/account-list-menu/hidden-account-list'; -import { WALLET_DETAILS_ROUTE } from '../../../helpers/constants/routes'; import { matchesSearchPattern } from './utils'; export type MultichainAccountsTreeProps = { @@ -47,16 +46,6 @@ export const MultichainAccountsTree = ({ }: MultichainAccountsTreeProps) => { const history = useHistory(); - const handleWalletDetailsClick = useCallback( - (walletId: string) => { - history.push( - WALLET_DETAILS_ROUTE.replace(':id', encodeURIComponent(walletId)), - ); - onClose(); - }, - [history, onClose], - ); - const accountsTree = useMemo(() => { // We keep a flag to check if there are any hidden accounts let hasHiddenAccounts: boolean = false; @@ -86,7 +75,6 @@ export const MultichainAccountsTree = ({ size={ButtonLinkSize.Sm} color={TextColor.primaryDefault} fontWeight={FontWeight.Medium} - onClick={() => handleWalletDetailsClick(walletId)} style={{ fontSize: '0.875rem', }} @@ -190,7 +178,6 @@ export const MultichainAccountsTree = ({ accountTreeItemProps, selectedAccount, onAccountTreeItemClick, - handleWalletDetailsClick, ]); return <>{accountsTree}; diff --git a/ui/components/multichain/account-details/account-details.tsx b/ui/components/multichain/account-details/account-details.tsx index 2239854b5ffc..7e1020f4b18b 100644 --- a/ui/components/multichain/account-details/account-details.tsx +++ b/ui/components/multichain/account-details/account-details.tsx @@ -23,11 +23,7 @@ import { getMetaMaskAccountsOrdered, getMetaMaskKeyrings, } from '../../../selectors'; -import { - clearAccountDetails, - hideWarning, - setAccountDetailsAddress, -} from '../../../store/actions'; +import { clearAccountDetails, hideWarning } from '../../../store/actions'; import HoldToRevealModal from '../../app/modals/hold-to-reveal-modal/hold-to-reveal-modal'; import { Box, @@ -100,7 +96,6 @@ export const AccountDetails = ({ address }: AccountDetailsProps) => { const [privateKey, setPrivateKey] = useState(''); const onClose = useCallback(() => { - dispatch(setAccountDetailsAddress('')); dispatch(clearAccountDetails()); dispatch(hideWarning()); }, [dispatch]); diff --git a/ui/components/multichain/menu-items/account-details-menu-item.js b/ui/components/multichain/menu-items/account-details-menu-item.js index 40c3cec38d69..e1555dc92d74 100644 --- a/ui/components/multichain/menu-items/account-details-menu-item.js +++ b/ui/components/multichain/menu-items/account-details-menu-item.js @@ -1,9 +1,8 @@ import React, { useCallback, useContext } from 'react'; import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { setAccountDetailsAddress } from '../../../store/actions'; import { MenuItem } from '../../ui/menu'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -15,37 +14,21 @@ import { import { IconName, Text } from '../../component-library'; import { getSelectedAccountGroup } from '../../../selectors/multichain-accounts/account-tree'; import { getHDEntropyIndex } from '../../../selectors/selectors'; -import { - getIsMultichainAccountsState1Enabled, - getIsMultichainAccountsState2Enabled, -} from '../../../selectors/multichain-accounts/feature-flags'; -import { - ACCOUNT_DETAILS_ROUTE, - MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE, -} from '../../../helpers/constants/routes'; +import { MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE } from '../../../helpers/constants/routes'; export const AccountDetailsMenuItem = ({ metricsLocation, closeMenu, - address, textProps, }) => { const t = useI18nContext(); - const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const selectedAccountGroup = useSelector(getSelectedAccountGroup); const hdEntropyIndex = useSelector(getHDEntropyIndex); const history = useHistory(); - const isMultichainAccountsState1Enabled = useSelector( - getIsMultichainAccountsState1Enabled, - ); - const isMultichainAccountsState2Enabled = useSelector( - getIsMultichainAccountsState2Enabled, - ); const LABEL = t('accountDetails'); const handleNavigation = useCallback(() => { - dispatch(setAccountDetailsAddress(address)); trackEvent({ event: MetaMetricsEventName.AccountDetailsOpened, category: MetaMetricsEventCategory.Navigation, @@ -54,22 +37,16 @@ export const AccountDetailsMenuItem = ({ hd_entropy_index: hdEntropyIndex, }, }); - if (isMultichainAccountsState2Enabled) { - history.push( - `${MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE}/${encodeURIComponent(selectedAccountGroup)}`, - ); - } else if (isMultichainAccountsState1Enabled) { - history.push(`${ACCOUNT_DETAILS_ROUTE}/${address}`); - } + + history.push( + `${MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE}/${encodeURIComponent(selectedAccountGroup)}`, + ); + closeMenu?.(); }, [ - address, closeMenu, - dispatch, hdEntropyIndex, history, - isMultichainAccountsState1Enabled, - isMultichainAccountsState2Enabled, metricsLocation, selectedAccountGroup, trackEvent, diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 003ae369558e..4818e8960f57 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -67,8 +67,6 @@ export const MULTICHAIN_WALLET_DETAILS_PAGE_ROUTE = '/multichain-wallet-details-page'; export const MULTICHAIN_SMART_ACCOUNT_PAGE_ROUTE = '/multichain-smart-account'; export const NEW_ACCOUNT_ROUTE = '/new-account'; -export const ACCOUNT_DETAILS_ROUTE = '/account-details'; -export const ACCOUNT_DETAILS_QR_CODE_ROUTE = '/account-details/qr-code'; export const CONFIRM_ADD_SUGGESTED_NFT_ROUTE = '/confirm-add-suggested-nft'; export const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; export const SEND_ROUTE = '/send'; @@ -146,7 +144,6 @@ export const ONBOARDING_EXPERIMENTAL_AREA = '/onboarding/experimental-area'; ///: END:ONLY_INCLUDE_IF export const DEEP_LINK_ROUTE = '/link'; -export const WALLET_DETAILS_ROUTE = '/wallet-details/:id'; export const DEFI_ROUTE = '/defi'; export const SHIELD_PLAN_ROUTE = '/shield-plan'; @@ -328,16 +325,6 @@ export const ROUTES = [ label: 'New Account Page', trackInAnalytics: true, }, - { - path: ACCOUNT_DETAILS_ROUTE, - label: 'Account Details Page', - trackInAnalytics: true, - }, - { - path: ACCOUNT_DETAILS_QR_CODE_ROUTE, - label: 'Account Details QR Code Page', - trackInAnalytics: true, - }, { path: CONFIRM_ADD_SUGGESTED_NFT_ROUTE, label: 'Confirm Add Suggested NFT Page', @@ -539,11 +526,6 @@ export const ROUTES = [ label: 'Deep link Redirect Page', trackInAnalytics: true, }, - { - path: WALLET_DETAILS_ROUTE, - label: 'Wallet Details Page', - trackInAnalytics: true, - }, // Onboarding routes { path: ONBOARDING_ROUTE, label: 'Onboarding', trackInAnalytics: false }, { diff --git a/ui/pages/confirmations/context/confirm/index.tsx b/ui/pages/confirmations/context/confirm/index.tsx index ec864af82b11..844ce37d0b41 100644 --- a/ui/pages/confirmations/context/confirm/index.tsx +++ b/ui/pages/confirmations/context/confirm/index.tsx @@ -6,11 +6,9 @@ import React, { useMemo, useState, } from 'react'; -import { TransactionType } from '@metamask/transaction-controller'; import { QuoteResponse } from '@metamask/bridge-controller'; import { useDispatch } from 'react-redux'; -import { setAccountDetailsAddress } from '../../../../store/actions'; import useCurrentConfirmation from '../../hooks/useCurrentConfirmation'; import useSyncConfirmPath from '../../hooks/useSyncConfirmPath'; import { Confirmation } from '../../types/confirm'; @@ -62,19 +60,6 @@ export const ConfirmContextProvider: React.FC<{ ], ); - // The code below is added to close address details modal when opening confirmation from account details modal - // The was account details modal is build has a complexity in routing and closing it from within account details modal - // routes it back to home page which also closes confirmation modal. - useEffect(() => { - if ( - currentConfirmation && - (currentConfirmation.type === TransactionType.revokeDelegation || - currentConfirmation.type === TransactionType.batch) - ) { - dispatch(setAccountDetailsAddress('')); - } - }, [dispatch, currentConfirmation]); - return ( {children} ); diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 565468084238..7e8990be2d88 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -165,7 +165,6 @@ export default class Home extends PureComponent { fetchBuyableChains: PropTypes.func.isRequired, redirectAfterDefaultPage: PropTypes.object, clearRedirectAfterDefaultPage: PropTypes.func, - setAccountDetailsAddress: PropTypes.func, isSeedlessPasswordOutdated: PropTypes.bool, isPrimarySeedPhraseBackedUp: PropTypes.bool, showShieldEntryModal: PropTypes.bool, @@ -255,22 +254,13 @@ export default class Home extends PureComponent { } checkRedirectAfterDefaultPage() { - const { - redirectAfterDefaultPage, - history, - clearRedirectAfterDefaultPage, - setAccountDetailsAddress, - } = this.props; + const { redirectAfterDefaultPage, history, clearRedirectAfterDefaultPage } = + this.props; if ( redirectAfterDefaultPage?.shouldRedirect && redirectAfterDefaultPage?.path ) { - // Set the account details address if provided - if (redirectAfterDefaultPage?.address) { - setAccountDetailsAddress(redirectAfterDefaultPage.address); - } - history.push(redirectAfterDefaultPage.path); clearRedirectAfterDefaultPage(); } diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 6d5e4e4a2c89..db4087b33b9a 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -51,7 +51,6 @@ import { setNewTokensImportedError, setDataCollectionForMarketing, setEditedNetwork, - setAccountDetailsAddress, lookupSelectedNetworks, setPendingShieldCohort, } from '../../store/actions'; @@ -249,8 +248,6 @@ const mapDispatchToProps = (dispatch) => { fetchBuyableChains: () => dispatch(fetchBuyableChains()), clearRedirectAfterDefaultPage: () => dispatch(clearRedirectAfterDefaultPage()), - setAccountDetailsAddress: (address) => - dispatch(setAccountDetailsAddress(address)), lookupSelectedNetworks: () => dispatch(lookupSelectedNetworks()), setPendingShieldCohort: (cohort) => dispatch(setPendingShieldCohort(cohort)), diff --git a/ui/pages/multichain-accounts/account-details/account-type-utils.test.ts b/ui/pages/multichain-accounts/account-details/account-type-utils.test.ts deleted file mode 100644 index ff0b88cabd6b..000000000000 --- a/ui/pages/multichain-accounts/account-details/account-type-utils.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { - MOCK_ACCOUNT_BIP122_P2WPKH, - MOCK_ACCOUNT_EOA, - MOCK_ACCOUNT_ERC4337, - MOCK_ACCOUNT_HARDWARE, - MOCK_ACCOUNT_INSTITUTIONAL, - MOCK_ACCOUNT_PRIVATE_KEY, - MOCK_ACCOUNT_SOLANA_MAINNET, -} from '../../../../test/data/mock-accounts'; -import { - getAccountTypeCategory, - isEVMAccount, - isSolanaAccount, - isHardwareAccount, - isPrivateKeyAccount, - isInstitutionalEVMAccount, - isBitcoinAccount, -} from './account-type-utils'; - -describe('Account Type Utils', () => { - describe('getAccountTypeCategory', () => { - it('should return "evm" for EOA accounts', () => { - expect(getAccountTypeCategory(MOCK_ACCOUNT_EOA)).toBe('evm'); - }); - - it('should return "evm" for ERC-4337 accounts', () => { - expect(getAccountTypeCategory(MOCK_ACCOUNT_ERC4337)).toBe('evm'); - }); - - it('should return "solana" for Solana accounts', () => { - expect(getAccountTypeCategory(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe( - 'solana', - ); - }); - - it('should return "unknown" for null/undefined accounts', () => { - expect(getAccountTypeCategory(null as unknown as InternalAccount)).toBe( - 'unknown', - ); - expect( - getAccountTypeCategory(undefined as unknown as InternalAccount), - ).toBe('unknown'); - }); - }); - - describe('isEVMAccount', () => { - it('should return true for EOA accounts', () => { - expect(isEVMAccount(MOCK_ACCOUNT_EOA)).toBe(true); - }); - - it('should return true for ERC-4337 accounts', () => { - expect(isEVMAccount(MOCK_ACCOUNT_ERC4337)).toBe(true); - }); - - it('should return false for Solana accounts', () => { - expect(isEVMAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); - }); - }); - - describe('isSolanaAccount', () => { - it('should return true for Solana accounts', () => { - expect(isSolanaAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(true); - }); - - it('should return false for EOA accounts', () => { - expect(isSolanaAccount(MOCK_ACCOUNT_EOA)).toBe(false); - }); - - it('should return false for ERC-4337 accounts', () => { - expect(isSolanaAccount(MOCK_ACCOUNT_ERC4337)).toBe(false); - }); - }); - - describe('isHardwareAccount', () => { - it('should return true for hardware accounts', () => { - expect(isHardwareAccount(MOCK_ACCOUNT_HARDWARE)).toBe(true); - }); - - it('should return false for EOA accounts', () => { - expect(isHardwareAccount(MOCK_ACCOUNT_EOA)).toBe(false); - }); - - it('should return false for Solana accounts', () => { - expect(isHardwareAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); - }); - }); - - describe('isPrivateKeyAccount', () => { - it('should return true for private key accounts', () => { - expect(isPrivateKeyAccount(MOCK_ACCOUNT_PRIVATE_KEY)).toBe(true); - }); - - it('should return false for EOA accounts', () => { - expect(isPrivateKeyAccount(MOCK_ACCOUNT_EOA)).toBe(false); - }); - - it('should return false for Solana accounts', () => { - expect(isPrivateKeyAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); - }); - }); - - describe('isInstitutionalEVMAccount', () => { - it('should return true for institutional EVM accounts', () => { - expect(isInstitutionalEVMAccount(MOCK_ACCOUNT_INSTITUTIONAL)).toBe(true); - }); - - it('should return false for regular EOA accounts', () => { - expect(isInstitutionalEVMAccount(MOCK_ACCOUNT_EOA)).toBe(false); - }); - - it('should return false for regular ERC-4337 accounts', () => { - expect(isInstitutionalEVMAccount(MOCK_ACCOUNT_ERC4337)).toBe(false); - }); - }); - - describe('isBitcoinAccount', () => { - it('should return true for Bitcoin accounts', () => { - expect(isBitcoinAccount(MOCK_ACCOUNT_BIP122_P2WPKH)).toBe(true); - }); - - it('should return false for EOA accounts', () => { - expect(isBitcoinAccount(MOCK_ACCOUNT_EOA)).toBe(false); - }); - - it('should return false for Solana accounts', () => { - expect(isBitcoinAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); - }); - }); -}); diff --git a/ui/pages/multichain-accounts/account-details/account-type-utils.ts b/ui/pages/multichain-accounts/account-details/account-type-utils.ts deleted file mode 100644 index 87e462b729bf..000000000000 --- a/ui/pages/multichain-accounts/account-details/account-type-utils.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { - BtcAccountType, - SolAccountType, - TrxAccountType, - isEvmAccountType, -} from '@metamask/keyring-api'; -import { KeyringTypes } from '@metamask/keyring-controller'; -import { InternalAccount } from '@metamask/keyring-internal-api'; - -export type AccountTypeCategory = - | 'evm' - | 'solana' - | 'hardware' - | 'private-key' - | 'institutional-evm' - | 'bitcoin' - | 'tron' - | 'unknown'; - -/** - * Determines the account type category based on the account's type and keyring information - * - * @param account - */ -export const getAccountTypeCategory = ( - account: InternalAccount, -): AccountTypeCategory => { - if (!account) { - return 'unknown'; - } - - const { type, metadata } = account; - const keyringType = metadata?.keyring?.type as KeyringTypes; - const snapId = metadata?.snap?.id; - - // Hardware accounts (must be checked before EVM check) - if ( - keyringType && - [ - KeyringTypes.ledger, - KeyringTypes.trezor, - KeyringTypes.oneKey, - KeyringTypes.lattice, - KeyringTypes.qr, - ].includes(keyringType) - ) { - return 'hardware'; - } - - // Private key accounts (must be checked before EVM check) - if (keyringType === KeyringTypes.simple) { - return 'private-key'; - } - - // Institutional-EVM accounts (must be checked before EVM check) - if ( - keyringType === KeyringTypes.snap && - snapId === 'npm:@metamask/institutional-wallet-snap' - ) { - return 'institutional-evm'; - } - - // EVM accounts (EOA and ERC-4337) - general fallback - if (isEvmAccountType(type)) { - return 'evm'; - } - - // Solana accounts - if (type === SolAccountType.DataAccount) { - return 'solana'; - } - - // Bitcoin accounts - if (Object.values(BtcAccountType).includes(type as BtcAccountType)) { - return 'bitcoin'; - } - - // TRON accounts - if (type === TrxAccountType.Eoa) { - return 'tron'; - } - - return 'unknown'; -}; - -/** - * Checks if an account is an EVM account (EOA or ERC-4337) - * - * @param account - The internal account object to check. - */ -export const isEVMAccount = (account: InternalAccount): boolean => { - return getAccountTypeCategory(account) === 'evm'; -}; - -/** - * Checks if an account is a Solana account - * - * @param account - The internal account object to check. - */ -export const isSolanaAccount = (account: InternalAccount): boolean => { - return getAccountTypeCategory(account) === 'solana'; -}; - -/** - * Checks if an account is a hardware wallet account - * - * @param account - The internal account object to check. - */ -export const isHardwareAccount = (account: InternalAccount): boolean => { - return getAccountTypeCategory(account) === 'hardware'; -}; - -/** - * Checks if an account is a private key account - * - * @param account - The internal account object to check. - */ -export const isPrivateKeyAccount = (account: InternalAccount): boolean => { - return getAccountTypeCategory(account) === 'private-key'; -}; - -/** - * Checks if an account is an institutional EVM account - * - * @param account - The internal account object to check. - */ -export const isInstitutionalEVMAccount = ( - account: InternalAccount, -): boolean => { - return getAccountTypeCategory(account) === 'institutional-evm'; -}; - -/** - * Checks if an account is a Bitcoin account - * - * @param account - The internal account object to check. - */ -export const isBitcoinAccount = (account: InternalAccount): boolean => { - return getAccountTypeCategory(account) === 'bitcoin'; -}; - -/** - * Checks if an account is a Tron account - * - * @param account - The internal account object to check. - */ -export const isTronAccount = (account: InternalAccount): boolean => { - return getAccountTypeCategory(account) === 'tron'; -}; diff --git a/ui/pages/multichain-accounts/account-details/btc-account-details.tsx b/ui/pages/multichain-accounts/account-details/btc-account-details.tsx deleted file mode 100644 index 3db8bcb2bc2f..000000000000 --- a/ui/pages/multichain-accounts/account-details/btc-account-details.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; - -type BitcoinAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const BitcoinAccountDetails = ({ - address, - account, -}: BitcoinAccountDetailsProps) => { - return ; -}; diff --git a/ui/pages/multichain-accounts/account-details/evm-account-details.tsx b/ui/pages/multichain-accounts/account-details/evm-account-details.tsx deleted file mode 100644 index 09bf1d3be78a..000000000000 --- a/ui/pages/multichain-accounts/account-details/evm-account-details.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; -import { SmartContractAccountToggleSection } from '../../../components/multichain-accounts/smart-contract-account-toggle-section'; -import { ACCOUNT_DETAILS_ROUTE } from '../../../helpers/constants/routes'; -import { AccountShowSrpRow } from '../../../components/multichain-accounts/account-show-srp-row/account-show-srp-row'; -import { Box } from '../../../components/component-library'; -import { AccountShowPrivateKeyRow } from '../../../components/multichain-accounts/account-show-private-key-row/account-show-private-key-row'; - -type EVMAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const EVMAccountDetails = ({ - address, - account, -}: EVMAccountDetailsProps) => { - return ( - - - - - - - - ); -}; diff --git a/ui/pages/multichain-accounts/account-details/hardware-account-details.tsx b/ui/pages/multichain-accounts/account-details/hardware-account-details.tsx deleted file mode 100644 index 7b734a0518d1..000000000000 --- a/ui/pages/multichain-accounts/account-details/hardware-account-details.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; -import { SmartContractAccountToggleSection } from '../../../components/multichain-accounts/smart-contract-account-toggle-section'; -import { ACCOUNT_DETAILS_ROUTE } from '../../../helpers/constants/routes'; - -type HardwareAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const HardwareAccountDetails = ({ - address, - account, -}: HardwareAccountDetailsProps) => { - return ( - - - - ); -}; diff --git a/ui/pages/multichain-accounts/account-details/index.ts b/ui/pages/multichain-accounts/account-details/index.ts deleted file mode 100644 index f4138da185af..000000000000 --- a/ui/pages/multichain-accounts/account-details/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { MultichainAccountDetails } from './multichain-account-details'; -export { EVMAccountDetails } from './evm-account-details'; -export { SolanaAccountDetails } from './solana-account-details'; -export { HardwareAccountDetails } from './hardware-account-details'; -export { PrivateKeyAccountDetails } from './private-key-account-details'; -export { InstitutionalEVMAccountDetails } from './institutional-evm-account-details'; -export { BitcoinAccountDetails } from './btc-account-details'; -export * from './account-type-utils'; diff --git a/ui/pages/multichain-accounts/account-details/institutional-evm-account-details.tsx b/ui/pages/multichain-accounts/account-details/institutional-evm-account-details.tsx deleted file mode 100644 index 9545f0d9a4c3..000000000000 --- a/ui/pages/multichain-accounts/account-details/institutional-evm-account-details.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; - -type InstitutionalEVMAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const InstitutionalEVMAccountDetails = ({ - address, - account, -}: InstitutionalEVMAccountDetailsProps) => { - return ; -}; diff --git a/ui/pages/multichain-accounts/account-details/multichain-account-details.test.tsx b/ui/pages/multichain-accounts/account-details/multichain-account-details.test.tsx deleted file mode 100644 index 31449ceaea7c..000000000000 --- a/ui/pages/multichain-accounts/account-details/multichain-account-details.test.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import { MemoryRouter, Route } from 'react-router-dom'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { renderWithProvider } from '../../../../test/jest'; -import { - MOCK_ACCOUNT_EOA, - MOCK_ACCOUNT_ERC4337, - MOCK_ACCOUNT_SOLANA_MAINNET, -} from '../../../../test/data/mock-accounts'; -import { MultichainAccountDetails } from './multichain-account-details'; - -const middleware = [thunk]; -const mockStore = configureMockStore(middleware); - -const createMockState = (address: string, account = MOCK_ACCOUNT_EOA) => ({ - appState: { - accountDetailsAddress: address, - }, - activeTab: { - origin: 'test', - }, - metamask: { - internalAccounts: { - accounts: { - [account.id]: { - ...account, - address, - }, - }, - selectedAccount: account.id, - }, - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - rpcEndpoints: [ - { - networkClientId: 'mainnet', - url: 'https://mainnet.infura.io/v3/', - type: 'infura', - }, - ], - defaultRpcEndpointIndex: 0, - blockExplorerUrls: ['https://etherscan.io'], - defaultBlockExplorerUrlIndex: 0, - }, - }, - selectedNetworkClientId: 'mainnet', - networksMetadata: { - mainnet: { - status: 'available', - }, - }, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [address], - metadata: { - id: 'keyring1', - name: 'HD Key Tree', - }, - }, - ], - accountTree: { - wallets: { - 'wallet:1': { - metadata: { - name: 'Wallet 1', - }, - groups: { - 'group:1': { - metadata: { - name: 'Group 1', - }, - accounts: [account.id], - }, - }, - }, - }, - }, - accountsByChainId: { - '0x1': { - [address]: { - balance: '0x0', - }, - }, - }, - pinnedAccounts: [], - hiddenAccounts: [], - permissionHistory: {}, - }, -}); - -describe('AccountDetails', () => { - describe('Account Type Detection', () => { - it('should render EVM account details for EOA accounts', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address, MOCK_ACCOUNT_EOA); - const store = mockStore(state); - - renderWithProvider( - - - - - , - store, - ); - - // Should render the base account details (which includes account name in header and details) - const accountNameElements = screen.getAllByText('Account 1'); - expect(accountNameElements).toHaveLength(2); - }); - - it('should render EVM account details for ERC-4337 accounts', () => { - const state = createMockState( - MOCK_ACCOUNT_ERC4337.address, - MOCK_ACCOUNT_ERC4337, - ); - const store = mockStore(state); - - renderWithProvider( - - - - - , - store, - ); - - // Should render the base account details (which includes account name in header and details) - const accountNameElements = screen.getAllByText('Account 2'); - expect(accountNameElements).toHaveLength(2); - }); - - it('should render account details for Solana accounts', () => { - const state = createMockState( - MOCK_ACCOUNT_SOLANA_MAINNET.address, - MOCK_ACCOUNT_SOLANA_MAINNET, - ); - const store = mockStore(state); - - renderWithProvider( - - - - - , - store, - ); - - // Should render the base account details (which includes account name in header and details) - const accountNameElements = screen.getAllByText('Solana Account'); - expect(accountNameElements).toHaveLength(2); - }); - }); -}); diff --git a/ui/pages/multichain-accounts/account-details/multichain-account-details.tsx b/ui/pages/multichain-accounts/account-details/multichain-account-details.tsx deleted file mode 100644 index 2f5a87bbd7cd..000000000000 --- a/ui/pages/multichain-accounts/account-details/multichain-account-details.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { getInternalAccountByAddress } from '../../../selectors'; -import { EVMAccountDetails } from './evm-account-details'; -import { getAccountTypeCategory } from './account-type-utils'; -import { SolanaAccountDetails } from './solana-account-details'; -import { HardwareAccountDetails } from './hardware-account-details'; -import { PrivateKeyAccountDetails } from './private-key-account-details'; -import { InstitutionalEVMAccountDetails } from './institutional-evm-account-details'; -import { BitcoinAccountDetails } from './btc-account-details'; -import { TronAccountDetails } from './tron-account-details'; - -export const MultichainAccountDetails = () => { - const { address } = useParams(); - const account = useSelector((state) => - getInternalAccountByAddress(state, address), - ); - - const accountTypeCategory = getAccountTypeCategory(account); - - const renderAccountDetailsByType = () => { - switch (accountTypeCategory) { - case 'evm': - return ( - - ); - - case 'solana': - return ( - - ); - - case 'hardware': - return ( - - ); - - case 'private-key': - return ( - - ); - - case 'institutional-evm': - return ( - - ); - - case 'bitcoin': - return ( - - ); - - case 'tron': - return ( - - ); - - default: - return null; - } - }; - - return renderAccountDetailsByType(); -}; diff --git a/ui/pages/multichain-accounts/account-details/private-key-account-details.tsx b/ui/pages/multichain-accounts/account-details/private-key-account-details.tsx deleted file mode 100644 index 01a4cc4257de..000000000000 --- a/ui/pages/multichain-accounts/account-details/private-key-account-details.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; -import { SmartContractAccountToggleSection } from '../../../components/multichain-accounts/smart-contract-account-toggle-section'; -import { ACCOUNT_DETAILS_ROUTE } from '../../../helpers/constants/routes'; -import { AccountShowPrivateKeyRow } from '../../../components/multichain-accounts/account-show-private-key-row/account-show-private-key-row'; -import { Box } from '../../../components/component-library'; - -type PrivateKeyAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const PrivateKeyAccountDetails = ({ - address, - account, -}: PrivateKeyAccountDetailsProps) => { - return ( - - - - - - - ); -}; diff --git a/ui/pages/multichain-accounts/account-details/solana-account-details.tsx b/ui/pages/multichain-accounts/account-details/solana-account-details.tsx deleted file mode 100644 index 150d9d0a783d..000000000000 --- a/ui/pages/multichain-accounts/account-details/solana-account-details.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; -import { AccountShowSrpRow } from '../../../components/multichain-accounts/account-show-srp-row/account-show-srp-row'; -import { Box } from '../../../components/component-library'; - -type SolanaAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const SolanaAccountDetails = ({ - address, - account, -}: SolanaAccountDetailsProps) => { - return ( - - - - - - ); -}; diff --git a/ui/pages/multichain-accounts/account-details/tron-account-details.tsx b/ui/pages/multichain-accounts/account-details/tron-account-details.tsx deleted file mode 100644 index 01991de7b654..000000000000 --- a/ui/pages/multichain-accounts/account-details/tron-account-details.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { BaseAccountDetails } from '../base-account-details/base-account-details'; -import { AccountShowSrpRow } from '../../../components/multichain-accounts/account-show-srp-row/account-show-srp-row'; -import { Box } from '../../../components/component-library'; - -type TronAccountDetailsProps = { - address: string; - account: InternalAccount; -}; - -export const TronAccountDetails = ({ - address, - account, -}: TronAccountDetailsProps) => { - return ( - - - - - - ); -}; diff --git a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.scss b/ui/pages/multichain-accounts/address-qr-code/address-qr-code.scss deleted file mode 100644 index c43867f4e0b4..000000000000 --- a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.scss +++ /dev/null @@ -1,3 +0,0 @@ -.address-qr-code-page { - max-width: 600px; -} diff --git a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.stories.tsx b/ui/pages/multichain-accounts/address-qr-code/address-qr-code.stories.tsx deleted file mode 100644 index 3965205d491a..000000000000 --- a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.stories.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router-dom'; -import { EthAccountType, SolAccountType } from '@metamask/keyring-api'; -import { KeyringTypes } from '@metamask/keyring-controller'; -import configureStore from '../../../store/store'; -import { Box } from '../../../components/component-library'; -import { AddressQRCode } from './address-qr-code'; - -// Mock Ethereum Account -const MOCK_ETH_ACCOUNT = { - id: 'mock-eth-account-id', - address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F', - metadata: { - name: 'Account 1', - keyring: { - type: KeyringTypes.hd, - }, - importTime: Date.now(), - }, - options: {}, - methods: [ - 'personal_sign', - 'eth_sign', - 'eth_signTransaction', - 'eth_signTypedData_v1', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - ], - type: EthAccountType.Eoa, -}; - -// Mock Solana Account -const MOCK_SOLANA_ACCOUNT = { - id: 'mock-solana-account-id', - address: 'DdHGa63k3vcH6kqDbX834GpeRUUef81Q8bUrBPdF937k', - metadata: { - name: 'Solana Account 1', - keyring: { - type: KeyringTypes.snap, - }, - snap: { - id: 'npm:@solana/wallet-snap', - name: 'Solana Wallet', - enabled: true, - }, - importTime: Date.now(), - }, - options: { - entropySource: 'mock-hd-keyring-id', - }, - methods: [ - 'solana_signTransaction', - 'solana_signAllTransactions', - 'solana_signMessage', - ], - type: SolAccountType.DataAccount, -}; - -// Minimal mock store data with network configurations -const createBaseMockStore = (account, address) => ({ - appState: { - accountDetailsAddress: address, - }, - metamask: { - internalAccounts: { - accounts: { - [account.id]: account, - }, - selectedAccount: account.id, - }, - accounts: { - [account.address]: { - address: account.address, - balance: '0x1bc16d674ec80000', // 2 ETH in hex - }, - }, - keyrings: [ - { - type: KeyringTypes.hd, - accounts: account.type === EthAccountType.Eoa ? [account.address] : [], - }, - { - type: KeyringTypes.snap, - accounts: account.type === SolAccountType.DataAccount ? [account.address] : [], - }, - ], - useBlockie: false, - providerConfig: { - chainId: '0x1', - type: 'mainnet', - rpcUrl: account.type === EthAccountType.Eoa - ? 'https://mainnet.infura.io/v3/abc123' - : 'https://api.mainnet-beta.solana.com', - nickname: account.type === EthAccountType.Eoa ? 'Ethereum Mainnet' : 'Solana Mainnet', - ticker: account.type === EthAccountType.Eoa ? 'ETH' : 'SOL', - rpcPrefs: { - blockExplorerUrl: account.type === EthAccountType.Eoa - ? 'https://etherscan.io' - : 'https://explorer.solana.com', - }, - }, - networkConfigurations: { - mainnet: { - chainId: '0x1', - nickname: 'Ethereum Mainnet', - rpcUrl: 'https://mainnet.infura.io/v3/abc123', - ticker: 'ETH', - type: 'custom', - blockExplorerUrl: 'https://etherscan.io', - rpcEndpoints: [ - { - url: 'https://mainnet.infura.io/v3/abc123', - type: 'custom', - networkClientId: 'mainnet', - }, - ], - defaultRpcEndpointIndex: 0, - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - }, - }, - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - nickname: 'Ethereum Mainnet', - rpcUrl: 'https://mainnet.infura.io/v3/abc123', - ticker: 'ETH', - type: 'custom', - blockExplorerUrl: 'https://etherscan.io', - rpcEndpoints: [ - { - url: 'https://mainnet.infura.io/v3/abc123', - type: 'custom', - networkClientId: 'mainnet', - }, - ], - defaultRpcEndpointIndex: 0, - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - }, - }, - selectedNetworkClientId: 'mainnet', - isEvmSelected: account.type !== SolAccountType.DataAccount, - selectedMultichainNetworkChainId: account.type === SolAccountType.DataAccount - ? 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' - : '0x1', - multichainNetworkConfigurationsByChainId: account.type === SolAccountType.DataAccount ? { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - nativeCurrency: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501', - isEvm: false, - }, - } : {}, - }, -}); - -// Story wrapper component -function StoryWrapper({ children, mockStore, address }) { - return ( - - - - - - - {children} - - - - - - - ); -} - -export default { - title: 'Pages/MultichainAccounts/AddressQRCode', - component: AddressQRCode, -}; - -// Ethereum Account QR Code Story -export const EthereumAddressQR = { - render: () => ( - - - - ), -}; - -// Solana Account QR Code Story -export const SolanaAddressQR = { - render: () => ( - - - - ), -}; diff --git a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.test.tsx b/ui/pages/multichain-accounts/address-qr-code/address-qr-code.test.tsx deleted file mode 100644 index c7f7f0175778..000000000000 --- a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.test.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { ACCOUNT_DETAILS_QR_CODE_ROUTE } from '../../../helpers/constants/routes'; -import { openBlockExplorer } from '../../../components/multichain/menu-items/view-explorer-menu-item'; -import { getMultichainAccountUrl } from '../../../helpers/utils/multichain/blockExplorer'; -import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { AddressQRCode } from './address-qr-code'; - -// Mock the block explorer utility -jest.mock( - '../../../components/multichain/menu-items/view-explorer-menu-item', - () => ({ - openBlockExplorer: jest.fn(), - }), -); - -// Mock the multichain block explorer helper -jest.mock('../../../helpers/utils/multichain/blockExplorer', () => ({ - getMultichainAccountUrl: jest.fn(), -})); - -// Mock the useMultichainSelector hook -jest.mock('../../../hooks/useMultichainSelector', () => ({ - useMultichainSelector: jest.fn(), -})); - -// Mock React Router -const mockHistoryGoBack = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - goBack: mockHistoryGoBack, - }), -})); - -// Mock i18n -jest.mock('../../../hooks/useI18nContext', () => ({ - useI18nContext: () => (key: string, substitutions?: string[]) => { - const translations: Record = { - address: '[address]', - viewOnExplorer: 'View on explorer', - viewAddressOnExplorer: `View on ${substitutions?.[0]}`, - }; - return translations[key] || key; - }, -})); - -const mockStore = configureStore([thunk]); -const mockTrackEvent = jest.fn(); - -// Cast the imported functions to mocked versions -const mockUseMultichainSelector = useMultichainSelector as jest.MockedFunction< - typeof useMultichainSelector ->; -const mockGetMultichainAccountUrl = - getMultichainAccountUrl as jest.MockedFunction< - typeof getMultichainAccountUrl - >; -const mockOpenBlockExplorer = openBlockExplorer as jest.MockedFunction< - typeof openBlockExplorer ->; - -const mockBlockExplorerUrl = - 'https://etherscan.io/address/0x1234567890abcdef1234567890abcdef12345678'; - -const mockAccount = { - id: 'account-1', - address: '0x1234567890abcdef1234567890abcdef12345678', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: [], - type: 'eip155:eoa', -}; - -const mockMultichainNetwork = { - chainId: 'eip155:1', - name: 'Ethereum Mainnet', - nativeCurrency: { - symbol: 'ETH', - name: 'Ethereum', - decimals: 18, - }, - blockExplorerUrls: ['https://etherscan.io'], -}; - -const mockState = { - appState: { - accountDetailsAddress: mockAccount.address, - }, - metamask: { - internalAccounts: { - accounts: { - [mockAccount.id]: mockAccount, - }, - selectedAccount: mockAccount.id, - }, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [mockAccount.address], - }, - ], - }, -}; - -const renderComponent = (state = mockState, address = mockAccount.address) => { - const store = mockStore(state); - - return render( - - - - - - - - - , - ); -}; - -describe('AddressQRCode', () => { - beforeEach(() => { - jest.clearAllMocks(); - - // Mock the multichain selector - mockUseMultichainSelector.mockReturnValue(mockMultichainNetwork); - - // Mock the block explorer URL helper - mockGetMultichainAccountUrl.mockReturnValue(mockBlockExplorerUrl); - }); - - describe('Component Rendering', () => { - it('should render the page with back button', () => { - renderComponent(); - - expect(screen.getByLabelText('Back')).toBeInTheDocument(); - }); - - it('should render view on etherscan button', () => { - renderComponent(); - - const explorerButton = screen.getByRole('button', { - name: 'View on Etherscan', - }); - expect(explorerButton).toBeInTheDocument(); - }); - }); - - describe('Navigation', () => { - it('should navigate back to account details when back button is clicked', () => { - renderComponent(); - - const backButton = screen.getByLabelText('Back'); - fireEvent.click(backButton); - - expect(mockHistoryGoBack).toHaveBeenCalledTimes(1); - }); - }); - - describe('Block Explorer Integration', () => { - it('should open block explorer when view on etherscan button is clicked', async () => { - renderComponent(); - - const explorerButton = screen.getByRole('button', { - name: 'View on Etherscan', - }); - fireEvent.click(explorerButton); - - await waitFor(() => { - expect(mockOpenBlockExplorer).toHaveBeenCalledWith( - mockBlockExplorerUrl, - 'Account Details QR Code Page', - mockTrackEvent, - ); - }); - }); - }); -}); diff --git a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.tsx b/ui/pages/multichain-accounts/address-qr-code/address-qr-code.tsx deleted file mode 100644 index b999dfaf641c..000000000000 --- a/ui/pages/multichain-accounts/address-qr-code/address-qr-code.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useCallback, useContext } from 'react'; -import { parseCaipChainId } from '@metamask/utils'; -import { useSelector } from 'react-redux'; -import { useHistory, useParams } from 'react-router-dom'; -import { - Page, - Header, - Content, - Footer, -} from '../../../components/multichain/pages/page'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { - ButtonIcon, - ButtonIconSize, - ButtonSecondary, - ButtonSecondarySize, - IconName, -} from '../../../components/component-library'; -import { - BackgroundColor, - TextVariant, -} from '../../../helpers/constants/design-system'; -import QrCodeView from '../../../components/ui/qr-code-view'; -import { getInternalAccountByAddress } from '../../../selectors'; -import { getMultichainNetwork } from '../../../selectors/multichain'; -import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; -import { getMultichainAccountUrl } from '../../../helpers/utils/multichain/blockExplorer'; -import { openBlockExplorer } from '../../../components/multichain/menu-items/view-explorer-menu-item'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - MetaMetricsEventName, - MetaMetricsEventCategory, -} from '../../../../shared/constants/metametrics'; -import { getAccountTypeCategory } from '../account-details'; - -export const AddressQRCode = () => { - const t = useI18nContext(); - const history = useHistory(); - const { address } = useParams(); - const trackEvent = useContext(MetaMetricsContext); - const account = useSelector((state) => - getInternalAccountByAddress(state, address), - ); - - const multichainNetwork = useMultichainSelector( - getMultichainNetwork, - account, - ); - - const addressLink = getMultichainAccountUrl( - account.address, - multichainNetwork, - ); - - const chainId = parseCaipChainId(multichainNetwork.chainId).reference; - - const metricsLocation = 'Account Details QR Code Page'; - - const handleNavigation = useCallback(() => { - trackEvent({ - event: MetaMetricsEventName.BlockExplorerLinkClicked, - category: MetaMetricsEventCategory.Accounts, - properties: { - location: metricsLocation, - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - chain_id: chainId, - }, - }); - openBlockExplorer(addressLink, metricsLocation, trackEvent); - }, [chainId, trackEvent, addressLink]); - - const getExplorerButtonText = (): string => { - switch (getAccountTypeCategory(account)) { - case 'evm': - return t('viewAddressOnExplorer', ['Etherscan']); - case 'solana': - return t('viewAddressOnExplorer', ['Solscan']); - default: - return t('viewOnExplorer'); - } - }; - - return ( - -
history.goBack()} - /> - } - > - {t('address')} -
- - - -
- - {getExplorerButtonText()} - -
-
- ); -}; diff --git a/ui/pages/multichain-accounts/address-qr-code/index.ts b/ui/pages/multichain-accounts/address-qr-code/index.ts deleted file mode 100644 index cb2e0bbc3190..000000000000 --- a/ui/pages/multichain-accounts/address-qr-code/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './address-qr-code'; diff --git a/ui/pages/multichain-accounts/base-account-details/base-account-details.scss b/ui/pages/multichain-accounts/base-account-details/base-account-details.scss deleted file mode 100644 index 519377799a26..000000000000 --- a/ui/pages/multichain-accounts/base-account-details/base-account-details.scss +++ /dev/null @@ -1,20 +0,0 @@ -.multichain-account-details-page { - max-width: 600px; - - .multichain-account-details__section { - .multichain-account-details__row { - margin-bottom: 1px; - - &:first-of-type { - border-top-left-radius: 8px; - border-top-right-radius: 8px; - } - - &:last-of-type { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - margin-bottom: 0; - } - } - } -} diff --git a/ui/pages/multichain-accounts/base-account-details/base-account-details.stories.tsx b/ui/pages/multichain-accounts/base-account-details/base-account-details.stories.tsx deleted file mode 100644 index d8d6c141d5a3..000000000000 --- a/ui/pages/multichain-accounts/base-account-details/base-account-details.stories.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { EthAccountType, SolAccountType } from '@metamask/keyring-api'; -import { KeyringTypes } from '@metamask/keyring-controller'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import configureStore from '../../../store/store'; -import { Box } from '../../../components/component-library'; -import { BaseAccountDetails } from './base-account-details'; - -// Mock Ethereum Account -const MOCK_ETH_ACCOUNT = { - id: 'mock-eth-account-id', - address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F', - metadata: { - name: 'Account 1', - keyring: { - type: KeyringTypes.hd, - }, - importTime: Date.now(), - }, - options: {}, - methods: [ - 'personal_sign', - 'eth_sign', - 'eth_signTransaction', - 'eth_signTypedData_v1', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - ], - scopes: ['eip155:1'], - type: EthAccountType.Eoa, -} as InternalAccount; - -// Mock Solana Account -const MOCK_SOLANA_ACCOUNT = { - id: 'mock-solana-account-id', - address: 'DdHGa63k3vcH6kqDbX834GpeRUUef81Q8bUrBPdF937k', - metadata: { - name: 'Solana Account 1', - keyring: { - type: KeyringTypes.snap, - }, - snap: { - id: 'npm:@solana/wallet-snap', - name: 'Solana Wallet', - enabled: true, - }, - importTime: Date.now(), - }, - options: { - entropySource: 'mock-hd-keyring-id', - }, - methods: [ - 'solana_signTransaction', - 'solana_signAllTransactions', - 'solana_signMessage', - ], - scopes: ['solana:mainnet'] as const, - type: SolAccountType.DataAccount, -} as InternalAccount; - -// Minimal mock store data -const createBaseMockStore = (account, address, walletName = 'Mock Wallet') => ({ - appState: { - accountDetailsAddress: address, - }, - activeTab: { - id: 1, - title: 'Test Dapp', - origin: 'https://test-dapp.com', - protocol: 'https:', - url: 'https://test-dapp.com', - }, - metamask: { - useBlockie: false, - internalAccounts: { - accounts: { - [account.id]: { - ...account, - address, - }, - }, - selectedAccount: account.id, - }, - accountTree: { - wallets: { - 'mock-wallet-id': { - id: 'mock-wallet-id', - metadata: { - name: walletName, - }, - groups: { - 'mock-wallet-id:default': { - id: 'mock-wallet-id:default', - metadata: { - name: 'Default', - }, - accounts: [account.id], - }, - }, - }, - }, - }, - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - blockExplorerUrls: ['https://etherscan.io'], - rpcEndpoints: [ - { - url: 'https://mainnet.infura.io/v3/your-project-id', - type: 'infura', - networkClientId: 'mainnet', - }, - ], - defaultRpcEndpointIndex: 0, - defaultBlockExplorerUrlIndex: 0, - }, - }, - selectedNetworkClientId: 'mainnet', - accountsByChainId: { - '0x1': { - [address]: { - balance: '0x0', - address, - }, - }, - }, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [address], - index: 0, - metadata: { - id: 'mock-hd-keyring-id', - name: 'HD Key Tree', - }, - }, - ], - permissionHistory: { - 'https://test-dapp.com': { - eth_accounts: { - accounts: { - [address]: Date.now(), - }, - }, - }, - }, - pinnedAccountsList: [], - hiddenAccountsList: [], - connectedAccounts: [], - }, -}); - -// Story wrapper component similar to PendingApproval -function StoryWrapper({ children, mockStore }) { - return ( - - - - - {children} - - - - - ); -} - -export default { - title: 'Pages/MultichainAccounts/BaseAccountDetails', - component: BaseAccountDetails, -}; - -// Ethereum Account Story -export const EthereumAccount = { - render: () => ( - - - - ), -}; - -// Solana Account Story -export const SolanaAccount = { - render: () => ( - - - - ), -}; diff --git a/ui/pages/multichain-accounts/base-account-details/base-account-details.test.tsx b/ui/pages/multichain-accounts/base-account-details/base-account-details.test.tsx deleted file mode 100644 index 549fa1e066f2..000000000000 --- a/ui/pages/multichain-accounts/base-account-details/base-account-details.test.tsx +++ /dev/null @@ -1,516 +0,0 @@ -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { renderWithProvider } from '../../../../test/jest'; -import { - MOCK_ACCOUNT_EOA, - MOCK_ACCOUNT_SOLANA_MAINNET, -} from '../../../../test/data/mock-accounts'; -import { ACCOUNT_DETAILS_QR_CODE_ROUTE } from '../../../helpers/constants/routes'; -import { KeyringType } from '../../../../shared/constants/keyring'; -import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; -import { BaseAccountDetails } from './base-account-details'; - -const middleware = [thunk]; -const mockStore = configureMockStore(middleware); - -// Mock the useHistory hook -const mockPush = jest.fn(); -const mockGoBack = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - push: mockPush, - goBack: mockGoBack, - }), -})); - -const createMockState = ( - address: string, - account = MOCK_ACCOUNT_EOA, - useBlockie = false, - walletName = 'Mock Wallet', -) => ({ - appState: { - accountDetailsAddress: address, - }, - activeTab: { - id: 1, - title: 'Test Dapp', - origin: 'https://test-dapp.com', - protocol: 'https:', - url: 'https://test-dapp.com', - }, - metamask: { - useBlockie, - internalAccounts: { - accounts: { - [account.id]: { - ...account, - address, - }, - }, - selectedAccount: account.id, - }, - accountTree: { - wallets: { - 'mock-wallet-id': { - id: 'mock-wallet-id', - metadata: { - name: walletName, - }, - groups: { - 'mock-wallet-id:default': { - id: 'mock-wallet-id:default', - metadata: { - name: 'Default', - }, - accounts: [account.id], - }, - }, - }, - }, - }, - // Required for network selectors - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - blockExplorerUrls: ['https://etherscan.io'], - rpcEndpoints: [ - { - url: 'https://mainnet.infura.io/v3/your-project-id', - type: 'infura', - networkClientId: 'mainnet', - }, - ], - defaultRpcEndpointIndex: 0, - defaultBlockExplorerUrlIndex: 0, - }, - }, - selectedNetworkClientId: 'mainnet', - // Required for account balance selectors - accountsByChainId: { - '0x1': { - [address]: { - balance: '0x0', - address, - }, - }, - }, - // Required for keyring selectors - keyrings: [ - { - type: 'HD Key Tree', - accounts: [address], - index: 0, - metadata: { - id: 'mock-hd-keyring-id', - name: 'HD Key Tree', - }, - }, - ], - // Required for permission selectors - permissionHistory: { - 'https://test-dapp.com': { - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - eth_accounts: { - accounts: { - [address]: Date.now(), - }, - }, - }, - }, - // Required for account lists - pinnedAccountsList: [], - hiddenAccountsList: [], - connectedAccounts: [], - }, -}); - -describe('BaseAccountDetails', () => { - beforeEach(() => { - mockPush.mockClear(); - mockGoBack.mockClear(); - }); - - describe('Component Rendering', () => { - it('should render with EVM account correctly', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Check if account name is displayed in header - expect(screen.getAllByText('Account 1')).toHaveLength(2); // Header + details section - - // Check if account details section is rendered - expect(screen.getByText('Account name')).toBeInTheDocument(); - expect(screen.getByText('Address')).toBeInTheDocument(); - expect(screen.getByText('Wallet')).toBeInTheDocument(); - - // Check if shortened address is displayed (short address stays as-is) - expect(screen.getByText('0xa0b86...f5e4b')).toBeInTheDocument(); - - // Check if wallet name is displayed - expect(screen.getByText('Mock Wallet')).toBeInTheDocument(); - }); - - it('should render with non-EVM (Solana) account correctly', () => { - const state = createMockState( - MOCK_ACCOUNT_SOLANA_MAINNET.address, - MOCK_ACCOUNT_SOLANA_MAINNET, - ); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Check if Solana account name is displayed (allow multiple occurrences) - expect(screen.getAllByText('Solana Account')).toHaveLength(2); - - // Check if Solana address is displayed correctly (7 chars + ... + 5 chars) - expect(screen.getByText('8A4AptC...aaLGC')).toBeInTheDocument(); - - // Check if wallet name is displayed - expect(screen.getByText('Mock Wallet')).toBeInTheDocument(); - }); - - it('should render children when passed', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - -
Test Child Component
-
-
, - store, - ); - - expect(screen.getByTestId('test-child')).toBeInTheDocument(); - expect(screen.getByText('Test Child Component')).toBeInTheDocument(); - }); - }); - - describe('Navigation', () => { - it('should go back when back button is clicked', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - const backButton = screen.getByLabelText('Back'); - fireEvent.click(backButton); - - expect(mockGoBack).toHaveBeenCalledTimes(1); - }); - - it('should navigate to QR code route when address row is clicked', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Find all "next" buttons and click the first one (address row) - const nextButtons = screen.getAllByLabelText('Next'); - const addressRowButton = nextButtons[0]; - fireEvent.click(addressRowButton); - - expect(mockPush).toHaveBeenCalledWith( - `${ACCOUNT_DETAILS_QR_CODE_ROUTE}/${MOCK_ACCOUNT_EOA.address}`, - ); - }); - - it('should navigate to wallet details when wallet row is clicked', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Find all "next" buttons and click the second one (wallet row) - const nextButtons = screen.getAllByLabelText('Next'); - const walletRowButton = nextButtons[1]; - fireEvent.click(walletRowButton); - - expect(mockPush).toHaveBeenCalledWith('/wallet-details/mock-wallet-id'); - }); - }); - - describe('Account Name Editing', () => { - it('should open edit account name modal when edit button is clicked', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - const editButton = screen.getByLabelText('Edit'); - fireEvent.click(editButton); - - // Check if modal is opened by looking for the edit modal text - expect(screen.getByText('Edit account name')).toBeInTheDocument(); - }); - }); - - describe('Address Formatting', () => { - it('should display checksummed and shortened address for EVM accounts', () => { - const mockEvmAccount = { - ...MOCK_ACCOUNT_EOA, - address: '0xABCDEF1234567890ABCDEF1234567890ABCDEF12', - }; - const state = createMockState(mockEvmAccount.address, mockEvmAccount); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Should display shortened checksummed address (7 chars + ... + 5 chars) - expect(screen.getByText('0xabcde...def12')).toBeInTheDocument(); - }); - - it('should display non-EVM address as-is without checksumming', () => { - const state = createMockState( - MOCK_ACCOUNT_SOLANA_MAINNET.address, - MOCK_ACCOUNT_SOLANA_MAINNET, - ); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Should display shortened Solana address without checksumming (7 chars + ... + 5 chars) - expect(screen.getByText('8A4AptC...aaLGC')).toBeInTheDocument(); - }); - }); - - describe('Modal Integration', () => { - it('should render EditAccountNameModal with correct props when editing', () => { - const state = createMockState(MOCK_ACCOUNT_EOA.address); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - // Open the modal - const editButton = screen.getByLabelText('Edit'); - fireEvent.click(editButton); - - // Check if modal is rendered by looking for the modal text content - expect(screen.getByText('Edit account name')).toBeInTheDocument(); - }); - }); - - describe('Account Removal', () => { - it('should display remove account button for removable accounts', () => { - const hardwareAccount = { - ...MOCK_ACCOUNT_EOA, - id: 'hardware-account', - metadata: { - ...MOCK_ACCOUNT_EOA.metadata, - name: 'Hardware Account', - keyring: { - type: KeyringType.trezor, - }, - }, - }; - - const state = createMockState(hardwareAccount.address, hardwareAccount); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - const removeButton = screen.getByText('Remove account'); - expect(removeButton).toBeInTheDocument(); - - // Verify that the modal is triggered and present - fireEvent.click(removeButton); - - expect( - screen.getByText('This account will be removed from MetaMask.'), - ).toBeInTheDocument(); - expect( - screen.getByText( - 'Make sure you have the Secret Recovery Phrase or private key for this account before removing.', - ), - ).toBeInTheDocument(); - }); - - it('should not display remove account button for non-removable accounts', () => { - const hdKeyTreeAccount = { - ...MOCK_ACCOUNT_EOA, - id: 'hd-account', - metadata: { - ...MOCK_ACCOUNT_EOA.metadata, - name: 'HD Account', - keyring: { - type: KeyringType.hdKeyTree, - }, - }, - }; - - const state = createMockState(hdKeyTreeAccount.address, hdKeyTreeAccount); - const store = mockStore(state); - - renderWithProvider( - - - , - store, - ); - - const removeButton = screen.queryByText('Remove account'); - expect(removeButton).not.toBeInTheDocument(); - - const solanaAccount = { - ...MOCK_ACCOUNT_SOLANA_MAINNET, - id: 'solana-account', - metadata: { - ...MOCK_ACCOUNT_SOLANA_MAINNET.metadata, - name: 'Solana Account', - keyring: { - type: KeyringType.snap, - }, - }, - }; - - const solanaState = createMockState(solanaAccount.address, solanaAccount); - const solanaStore = mockStore(solanaState); - - renderWithProvider( - - - , - solanaStore, - ); - - const solanaRemoveButton = screen.queryByText('Remove account'); - expect(solanaRemoveButton).not.toBeInTheDocument(); - }); - - it('should not display remove account button for social login accounts', () => { - const hardwareAccount = { - ...MOCK_ACCOUNT_EOA, - id: 'hardware-account', - metadata: { - ...MOCK_ACCOUNT_EOA.metadata, - name: 'Hardware Account', - keyring: { - type: KeyringType.trezor, - }, - }, - }; - - const state = createMockState(hardwareAccount.address, hardwareAccount); - const store = mockStore({ - ...state, - metamask: { - ...state.metamask, - firstTimeFlowType: FirstTimeFlowType.socialCreate, - }, - }); - - renderWithProvider( - - - , - store, - ); - - const removeButton = screen.queryByText('Remove account'); - expect(removeButton).not.toBeInTheDocument(); - }); - }); -}); diff --git a/ui/pages/multichain-accounts/base-account-details/base-account-details.tsx b/ui/pages/multichain-accounts/base-account-details/base-account-details.tsx deleted file mode 100644 index 6f0d3919c9d8..000000000000 --- a/ui/pages/multichain-accounts/base-account-details/base-account-details.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { useCallback, useContext, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { isEvmAccountType } from '@metamask/keyring-api'; -import { InternalAccount } from '@metamask/keyring-internal-api'; -import { formatChainIdToCaip } from '@metamask/bridge-controller'; -import { AvatarAccountSize } from '@metamask/design-system-react'; -import { - getAccountTypeForKeyring, - getHardwareWalletType, - getHDEntropyIndex, - getIsSocialLoginFlow, - isSolanaAccount, -} from '../../../selectors'; -import { - Box, - Button, - ButtonIcon, - ButtonIconSize, - ButtonSize, - ButtonVariant, -} from '../../../components/component-library'; -import { - Content, - Header, - Page, -} from '../../../components/multichain/pages/page'; -import { - BackgroundColor, - BlockSize, - IconColor, -} from '../../../helpers/constants/design-system'; -import { - ACCOUNT_DETAILS_QR_CODE_ROUTE, - DEFAULT_ROUTE, -} from '../../../helpers/constants/routes'; -import { IconName } from '../../../components/component-library/icon'; -import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; -import { shortenAddress } from '../../../helpers/utils/util'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { AccountDetailsRow } from '../../../components/multichain-accounts/account-details-row'; -import { EditAccountNameModal } from '../../../components/multichain-accounts/edit-account-name-modal'; -import { - removeAccount, - setAccountDetailsAddress, -} from '../../../store/actions'; -import { getWalletIdAndNameByAccountAddress } from '../../../selectors/multichain-accounts/account-tree'; -import { WalletMetadata } from '../../../selectors/multichain-accounts/account-tree.types'; -import { KeyringType } from '../../../../shared/constants/keyring'; -import { AccountRemoveModal } from '../../../components/multichain-accounts/account-remove-modal'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { getCurrentChainId } from '../../../../shared/modules/selectors/networks'; -import { formatAccountType } from '../../../helpers/utils/metrics'; -import { PreferredAvatar } from '../../../components/app/preferred-avatar'; - -type BaseAccountDetailsProps = { - children?: React.ReactNode | React.ReactNode[]; - account: InternalAccount; - address: string; -}; - -export const BaseAccountDetails = ({ - children, - account, - address, -}: BaseAccountDetailsProps) => { - const history = useHistory(); - const dispatch = useDispatch(); - const t = useI18nContext(); - const trackEvent = useContext(MetaMetricsContext); - const chainId = useSelector(getCurrentChainId); - const hdEntropyIndex = useSelector(getHDEntropyIndex); - const deviceName = useSelector(getHardwareWalletType); - const socialLoginFlow = useSelector(getIsSocialLoginFlow); - - const { - metadata: { name }, - type, - } = account; - const formattedAddress = isEvmAccountType(type) - ? toChecksumHexAddress(address as string)?.toLowerCase() - : address; - const shortenedAddress = shortenAddress(formattedAddress); - - const [isEditingAccountName, setIsEditingAccountName] = useState(false); - - const handleShowAddress = () => { - history.push(`${ACCOUNT_DETAILS_QR_CODE_ROUTE}/${address}`); - }; - - const { keyring } = account.metadata; - const accountType = formatAccountType(getAccountTypeForKeyring(keyring)); - - const handleNavigation = useCallback(() => { - dispatch(setAccountDetailsAddress('')); - history.goBack(); - }, [history, dispatch]); - - // we can never have a scenario where an account is not associated with a wallet. - const { id: walletId, name: walletName } = useSelector((state) => - getWalletIdAndNameByAccountAddress(state, address), - ) as WalletMetadata; - - const walletRoute = `/wallet-details/${encodeURIComponent(walletId)}`; - - const isRemovable = - account.metadata.keyring.type !== KeyringType.hdKeyTree && - !isSolanaAccount(account) && - !socialLoginFlow; // social login accounts are not removable - - const [showAccountRemoveModal, setShowAccountRemoveModal] = useState(false); - - const handleAccountRemoveAction = useCallback(() => { - dispatch(removeAccount(account.address)); - - trackEvent({ - event: MetaMetricsEventName.AccountRemoved, - category: MetaMetricsEventCategory.Accounts, - properties: { - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - account_hardware_type: deviceName, - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - chain_id: chainId, - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - account_type: accountType, - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - hd_entropy_index: hdEntropyIndex, - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - caip_chain_id: formatChainIdToCaip(chainId), - }, - }); - - dispatch(setAccountDetailsAddress('')); - history.push(DEFAULT_ROUTE); - }, [ - dispatch, - account.address, - trackEvent, - deviceName, - chainId, - accountType, - hdEntropyIndex, - history, - ]); - - return ( - -
- } - > - {name} -
- - - - - - - } - onClick={() => setIsEditingAccountName(true)} - /> - - } - onClick={handleShowAddress} - /> - - } - onClick={() => { - history.push(walletRoute); - }} - /> - - {children} - {isRemovable && ( - - - - )} - {isEditingAccountName && ( - setIsEditingAccountName(false)} - currentAccountName={name} - address={address} - /> - )} - {showAccountRemoveModal && ( - setShowAccountRemoveModal(false)} - onSubmit={handleAccountRemoveAction} - accountName={account.metadata.name} - accountAddress={account.address} - /> - )} - -
- ); -}; diff --git a/ui/pages/multichain-accounts/base-account-details/index.ts b/ui/pages/multichain-accounts/base-account-details/index.ts deleted file mode 100644 index f0409ed377e1..000000000000 --- a/ui/pages/multichain-accounts/base-account-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BaseAccountDetails } from './base-account-details'; diff --git a/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.tsx b/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.tsx index 7cab1ed1269c..289632946efc 100644 --- a/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.tsx +++ b/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.tsx @@ -45,10 +45,7 @@ import { MultichainSrpBackup } from '../../../components/multichain-accounts/mul import { useWalletInfo } from '../../../hooks/multichain-accounts/useWalletInfo'; import { MultichainAccountEditModal } from '../../../components/multichain-accounts/multichain-account-edit-modal'; import { AccountRemoveModal } from '../../../components/multichain-accounts/account-remove-modal'; -import { - removeAccount, - setAccountDetailsAddress, -} from '../../../store/actions'; +import { removeAccount } from '../../../store/actions'; import { MetaMetricsEventCategory, MetaMetricsEventName, @@ -137,7 +134,6 @@ export const MultichainAccountDetailsPage = () => { }, }); - dispatch(setAccountDetailsAddress('')); history.push(DEFAULT_ROUTE); } }, [dispatch, trackEvent, history, wallet?.type, accountsWithAddresses]); diff --git a/ui/pages/multichain-accounts/wallet-details/index.ts b/ui/pages/multichain-accounts/wallet-details/index.ts deleted file mode 100644 index 0429f84e2c07..000000000000 --- a/ui/pages/multichain-accounts/wallet-details/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import WalletDetails from './wallet-details.component'; - -export default WalletDetails; diff --git a/ui/pages/multichain-accounts/wallet-details/wallet-details.component.stories.tsx b/ui/pages/multichain-accounts/wallet-details/wallet-details.component.stories.tsx deleted file mode 100644 index eb0f2123ff7d..000000000000 --- a/ui/pages/multichain-accounts/wallet-details/wallet-details.component.stories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { Provider } from 'react-redux'; -import configureStore from '../../../store/store'; -import testData from '../../../../.storybook/test-data'; -import WalletDetails from './wallet-details.component'; -import { MemoryRouter, Route } from 'react-router-dom'; - -// Mock the accountTree data that WalletDetails expects -const mockAccountTree = { - wallets: { - 'entropy:test-entropy-wallet': { - id: 'entropy:test-entropy-wallet', - metadata: { - name: 'Test Wallet', - }, - groups: { - 'entropy:test-entropy-wallet': { - id: 'entropy:test-entropy-wallet', - metadata: { - name: 'Default Group', - }, - accounts: ['cf8dace4-9439-4bd4-b3a8-88c821c8fcb3'], - }, - }, - }, - }, -}; - -const store = configureStore({ - ...testData, - metamask: { - ...testData.metamask, - // Add the accountTree to the metamask state - accountTree: mockAccountTree, - }, -}); - -const walletId = encodeURIComponent('entropy:test-entropy-wallet'); - -const meta: Meta = { - title: 'Pages/MultichainAccounts/WalletDetails', - component: WalletDetails, - decorators: [ - (story) => ( - - - {story()} - - - ), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/ui/pages/multichain-accounts/wallet-details/wallet-details.component.test.tsx b/ui/pages/multichain-accounts/wallet-details/wallet-details.component.test.tsx deleted file mode 100644 index 8ac40be36e8c..000000000000 --- a/ui/pages/multichain-accounts/wallet-details/wallet-details.component.test.tsx +++ /dev/null @@ -1,488 +0,0 @@ -import '@testing-library/jest-dom'; -import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { useHistory, useParams } from 'react-router-dom'; -import { SolScope } from '@metamask/keyring-api'; -import type { InternalAccount } from '@metamask/keyring-internal-api'; -import type { AccountGroupId, AccountWalletId } from '@metamask/account-api'; -import configureStore from '../../../store/store'; -import mockState from '../../../../test/data/mock-state.json'; -import { getWalletsWithAccounts } from '../../../selectors/multichain-accounts/account-tree'; -import { getIsPrimarySeedPhraseBackedUp } from '../../../ducks/metamask/metamask'; -import { getMetaMaskHdKeyrings } from '../../../selectors'; -import { createMockInternalAccount } from '../../../../test/jest/mocks'; -import WalletDetails from './wallet-details.component'; - -// Shared mock functions -const mockCreateAccount = jest.fn(); -const mockAddNewAccount = jest.fn(); -const mockSetAccountLabel = jest.fn(); -const mockGetNextAvailableAccountName = jest.fn(); - -// Shared mock clients -const mockSolanaClient = { createAccount: mockCreateAccount }; -const mockBitcoinClient = { createAccount: mockCreateAccount }; - -// Shared component mock factory -const createComponentMock = - (tag: string) => - ({ - children, - ...props - }: { - children?: React.ReactNode; - [key: string]: unknown; - }) => - React.createElement(tag, props, children); - -// Consolidated mocks -jest.mock('../../../hooks/useI18nContext', () => ({ - useI18nContext: () => (key: string) => key, -})); - -jest.mock('../../../hooks/accounts/useMultichainWalletSnapClient', () => ({ - WalletClientType: { - Bitcoin: 'bitcoin-wallet-snap', - Solana: 'solana-wallet-snap', - }, - EVM_WALLET_TYPE: 'evm', - useMultichainWalletSnapClient: jest.fn((clientType) => { - if (clientType === 'solana-wallet-snap') { - return mockSolanaClient; - } - if (clientType === 'bitcoin-wallet-snap') { - return mockBitcoinClient; - } - return null; - }), -})); - -jest.mock('../../../store/actions', () => ({ - setAccountDetailsAddress: jest.fn(() => ({ - type: 'SET_ACCOUNT_DETAILS_ADDRESS', - })), - addNewAccount: - (...args: unknown[]) => - () => - mockAddNewAccount(...args), - setAccountLabel: - (...args: unknown[]) => - () => - mockSetAccountLabel(...args), - getNextAvailableAccountName: (...args: unknown[]) => - mockGetNextAvailableAccountName(...args), -})); - -jest.mock('../../../selectors', () => ({ - getMetaMaskHdKeyrings: jest.fn(), - getIsBitcoinSupportEnabled: jest.fn(() => true), - getIsSolanaSupportEnabled: jest.fn(() => true), -})); - -jest.mock('react-router-dom', () => ({ - useHistory: jest.fn(), - useParams: jest.fn(), -})); - -jest.mock('../../../selectors/multichain-accounts/account-tree', () => ({ - getWalletsWithAccounts: jest.fn(), -})); - -jest.mock('../../../ducks/metamask/metamask', () => ({ - getIsPrimarySeedPhraseBackedUp: jest.fn(), -})); - -// Consolidated component mocks -jest.mock('../../../components/component-library', () => ({ - Box: createComponentMock('div'), - ButtonIcon: ({ - children, - ariaLabel, - ...props - }: { - children?: React.ReactNode; - ariaLabel?: string; - [key: string]: unknown; - }) => { - const buttonProps = { ...props }; - if (ariaLabel) { - buttonProps['aria-label'] = ariaLabel; - } - return createComponentMock('button')({ children, ...buttonProps }); - }, - ButtonIconSize: { Sm: 'sm' }, - Icon: createComponentMock('span'), - IconName: { ArrowLeft: 'arrow-left', ArrowRight: 'arrow-right', Add: 'add' }, - IconSize: { Sm: 'sm', Md: 'md' }, - IconColor: { iconAlternative: 'alternative', primaryDefault: 'primary' }, - Text: createComponentMock('span'), - BannerAlert: createComponentMock('div'), - BannerAlertSeverity: { Danger: 'danger' }, - Modal: ({ - children, - isOpen, - ...props - }: { - children?: React.ReactNode; - isOpen?: boolean; - [key: string]: unknown; - }) => (isOpen ?
{children}
: null), - ModalOverlay: createComponentMock('div'), -})); - -jest.mock( - '../../../components/component-library/modal-content/deprecated', - () => ({ - ModalContent: createComponentMock('div'), - }), -); - -jest.mock('../../../components/component-library/modal-header', () => ({ - ModalHeader: ({ - children, - onClose, - ...props - }: { - children?: React.ReactNode; - onClose?: () => void; - [key: string]: unknown; - }) => ( -
- {children} - -
- ), -})); - -jest.mock('../../../components/multichain/pages/page', () => ({ - Content: createComponentMock('div'), - Header: ({ - children, - startAccessory, - ...props - }: { - children?: React.ReactNode; - startAccessory?: React.ReactNode; - [key: string]: unknown; - }) => ( -
- {startAccessory} - {children} -
- ), - Page: createComponentMock('div'), -})); - -jest.mock( - '../../../components/multichain/multichain-accounts/wallet-details-account-item/wallet-details-account-item', - () => - ({ - account, - onClick, - className, - }: { - account: InternalAccount; - onClick: (account: InternalAccount) => void; - className: string; - }) => ( -
onClick(account)} - className={className} - > - {account.metadata.name} -
- ), -); - -jest.mock( - '../../../components/app/user-preferenced-currency-display/user-preferenced-currency-display.component', - () => - ({ value }: { value: string }) => ( -
{value}
- ), -); - -jest.mock( - '../../../components/app/srp-quiz-modal', - () => - ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => - isOpen ? ( -
- -
- ) : null, -); - -jest.mock( - '../../../components/multichain/multichain-accounts/wallet-details-account-type-selection', - () => ({ - WalletDetailsAccountTypeSelection: ({ - onAccountTypeSelect, - onClose, - }: { - onAccountTypeSelect: (accountType: string) => void; - onClose: () => void; - }) => ( -
- - - - -
- ), - }), -); - -describe('WalletDetails', () => { - const mockHistory = { push: jest.fn(), goBack: jest.fn() }; - const mockParams = { id: 'entropy:test-wallet' }; - const GROUP_ID = 'entropy:test-wallet:default' as unknown as AccountGroupId; - - // Helper functions - const createMockWallet = (id: string, groups: Record) => ({ - id: id as unknown as AccountWalletId, - metadata: { name: 'Test Wallet' }, - groups, - }); - - const createMockConsolidatedAccountGroup = ( - groupId: string, - accounts: InternalAccount[], - ) => ({ - id: groupId as unknown as AccountGroupId, - metadata: { name: 'Test Group' }, - accounts, - }); - - const setupMocks = (wallets: Record = {}) => { - const mockAccount = createMockInternalAccount({ - address: '0x123', - name: 'Test Account', - }); - const defaultWallet = createMockWallet('entropy:test-wallet', { - [GROUP_ID]: createMockConsolidatedAccountGroup(GROUP_ID, [mockAccount]), - }); - const mockWallets = { - ['entropy:test-wallet' as unknown as AccountWalletId]: defaultWallet, - ...wallets, - }; - - ( - getWalletsWithAccounts as jest.MockedFunction< - typeof getWalletsWithAccounts - > - ).mockReturnValue( - mockWallets as unknown as ReturnType, - ); - (getMetaMaskHdKeyrings as jest.Mock).mockReturnValue([ - { metadata: { id: 'test-wallet' } }, - ]); - (getIsPrimarySeedPhraseBackedUp as jest.Mock).mockReturnValue(true); - }; - - const setupEntropyWalletTest = (isFirstHdKeyring: boolean = false) => { - const entropyGroupId = - 'entropy:test-entropy-wallet:default' as unknown as AccountGroupId; - const entropyWallet = createMockWallet('entropy:test-entropy-wallet', { - [entropyGroupId]: createMockConsolidatedAccountGroup(entropyGroupId, [ - createMockInternalAccount({ - address: '0x123', - name: 'Test Account', - }), - ]), - }); - - ( - getWalletsWithAccounts as jest.MockedFunction< - typeof getWalletsWithAccounts - > - ).mockReturnValue({ - ['entropy:test-entropy-wallet' as unknown as AccountWalletId]: - entropyWallet, - } as unknown as ReturnType); - - (getMetaMaskHdKeyrings as jest.Mock).mockReturnValue([ - { - metadata: { - id: isFirstHdKeyring ? 'test-entropy-wallet' : 'other-wallet', - }, - }, - ]); - (getIsPrimarySeedPhraseBackedUp as jest.Mock).mockReturnValue(false); - (useParams as jest.Mock).mockReturnValue({ - id: 'entropy:test-entropy-wallet', - }); - - return render( - - - , - ); - }; - - const renderComponent = () => - render( - - - , - ); - - beforeEach(() => { - (useHistory as jest.Mock).mockReturnValue(mockHistory); - (useParams as jest.Mock).mockReturnValue(mockParams); - setupMocks(); - [ - mockAddNewAccount, - mockSetAccountLabel, - mockGetNextAvailableAccountName, - mockCreateAccount, - ].forEach((mock) => mock.mockClear()); - jest.spyOn(console, 'error').mockImplementation(() => undefined); - }); - - afterEach(() => { - jest.clearAllMocks(); - (console.error as jest.Mock).mockRestore(); - }); - - it('renders wallet details correctly', () => { - const { getByText } = renderComponent(); - expect(getByText('Test Wallet')).toBeInTheDocument(); - expect(getByText('Test Account')).toBeInTheDocument(); - }); - - it('shows SRP button for entropy wallets', () => { - const { getByText } = setupEntropyWalletTest(); - expect(getByText('secretRecoveryPhrase')).toBeInTheDocument(); - }); - - it('opens SRP quiz modal when SRP button is clicked', () => { - const { getByText, getByTestId } = setupEntropyWalletTest(); - fireEvent.click(getByText('secretRecoveryPhrase')); - expect(getByTestId('mock-srp-quiz')).toBeInTheDocument(); - }); - - it('dispatches setAccountDetailsAddress when account is clicked', () => { - const { getByText } = renderComponent(); - const { setAccountDetailsAddress } = jest.requireMock( - '../../../store/actions', - ); - fireEvent.click(getByText('Test Account')); - expect(setAccountDetailsAddress).toHaveBeenCalledWith('0x123'); - }); - - it('navigates back when back button is clicked', () => { - const { getByLabelText } = renderComponent(); - fireEvent.click(getByLabelText('back')); - expect(mockHistory.goBack).toHaveBeenCalled(); - }); - - describe('Add Account Button', () => { - it('renders add account button when wallet has accounts', () => { - const { getByText } = renderComponent(); - expect(getByText('addAccount')).toBeInTheDocument(); - }); - - it('does not render add account button when wallet has no accounts', () => { - const testWallet = createMockWallet('entropy:test-wallet', { - [GROUP_ID]: createMockConsolidatedAccountGroup(GROUP_ID, []), - }); - setupMocks({ - ['entropy:test-wallet' as unknown as AccountWalletId]: testWallet, - }); - const { queryByText } = renderComponent(); - expect(queryByText('addAccount')).not.toBeInTheDocument(); - }); - - it('opens account type selection modal when add account button is clicked', () => { - const { getByText, getByTestId } = renderComponent(); - fireEvent.click(getByText('addAccount')); - expect(getByTestId('mock-account-type-selection')).toBeInTheDocument(); - }); - - it('creates Ethereum account directly when Ethereum button is clicked', async () => { - const mockNewAccount = { address: '0x456', id: 'new-account-id' }; - mockAddNewAccount.mockResolvedValue(mockNewAccount); - - const { getByText, getByTestId, queryByTestId } = renderComponent(); - fireEvent.click(getByText('addAccount')); - fireEvent.click(getByTestId('select-ethereum-account')); - - await waitFor(() => { - expect(mockAddNewAccount).toHaveBeenCalledWith('test-wallet', false); - expect( - queryByTestId('mock-account-type-selection'), - ).not.toBeInTheDocument(); - }); - }); - - it('handles Ethereum account creation error gracefully', async () => { - const error = new Error('Account creation failed'); - mockAddNewAccount.mockRejectedValue(error); - mockGetNextAvailableAccountName.mockResolvedValue('Account 2'); - - const { getByText, getByTestId } = renderComponent(); - fireEvent.click(getByText('addAccount')); - fireEvent.click(getByTestId('select-ethereum-account')); - - await waitFor(() => { - expect(mockAddNewAccount).toHaveBeenCalledWith('test-wallet', false); - expect(mockSetAccountLabel).not.toHaveBeenCalled(); - }); - }); - - it('uses correct keyringId for entropy wallets', async () => { - const mockNewAccount = { address: '0x456', id: 'new-account-id' }; - mockAddNewAccount.mockResolvedValue(mockNewAccount); - mockGetNextAvailableAccountName.mockResolvedValue('Account 2'); - - const { getByText, getByTestId } = setupEntropyWalletTest(true); - fireEvent.click(getByText('addAccount')); - fireEvent.click(getByTestId('select-ethereum-account')); - - await waitFor(() => { - expect(mockAddNewAccount).toHaveBeenCalledWith( - 'test-entropy-wallet', - false, - ); - }); - }); - - it('creates Solana account directly when Solana button is clicked', async () => { - const { getByText, getByTestId, queryByTestId } = renderComponent(); - fireEvent.click(getByText('addAccount')); - fireEvent.click(getByTestId('select-solana-account')); - - await waitFor(() => { - expect(mockCreateAccount).toHaveBeenCalledWith( - { scope: SolScope.Mainnet, entropySource: 'test-wallet' }, - { - displayConfirmation: false, - displayAccountNameSuggestion: false, - setSelectedAccount: false, - }, - ); - expect( - queryByTestId('mock-account-type-selection'), - ).not.toBeInTheDocument(); - }); - }); - }); -}); diff --git a/ui/pages/multichain-accounts/wallet-details/wallet-details.component.tsx b/ui/pages/multichain-accounts/wallet-details/wallet-details.component.tsx deleted file mode 100644 index 8ae8d586bee8..000000000000 --- a/ui/pages/multichain-accounts/wallet-details/wallet-details.component.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -import { useSelector, useDispatch } from 'react-redux'; -import { AccountWalletId } from '@metamask/account-api'; -import { CaipChainId } from '@metamask/utils'; -import { - SolScope, - ///: BEGIN:ONLY_INCLUDE_IF(bitcoin) - BtcScope, - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(tron) - TrxScope, - ///: END:ONLY_INCLUDE_IF -} from '@metamask/keyring-api'; -import { - Box, - ButtonIcon, - ButtonIconSize, - Icon, - IconName, - IconSize, - Text, - BannerAlert, - BannerAlertSeverity, - Modal, - ModalOverlay, -} from '../../../components/component-library'; -import { ModalContent } from '../../../components/component-library/modal-content/deprecated'; -import { - AlignItems, - BlockSize, - IconColor, - Display, - TextVariant, - TextColor, - TextAlign, - JustifyContent, - BackgroundColor, - FlexDirection, -} from '../../../helpers/constants/design-system'; -import { - Content, - Header, - Page, -} from '../../../components/multichain/pages/page'; -import { getMetaMaskHdKeyrings } from '../../../selectors'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { getWalletsWithAccounts } from '../../../selectors/multichain-accounts/account-tree'; -import { getIsPrimarySeedPhraseBackedUp } from '../../../ducks/metamask/metamask'; -import WalletDetailsAccountItem from '../../../components/multichain/multichain-accounts/wallet-details-account-item/wallet-details-account-item'; -import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display/user-preferenced-currency-display.component'; -import SRPQuiz from '../../../components/app/srp-quiz-modal'; -import { WalletDetailsAccountTypeSelection } from '../../../components/multichain/multichain-accounts/wallet-details-account-type-selection'; -import { - setAccountDetailsAddress, - addNewAccount, -} from '../../../store/actions'; -import { - ACCOUNT_DETAILS_ROUTE, - ONBOARDING_REVIEW_SRP_ROUTE, -} from '../../../helpers/constants/routes'; -import { endTrace, trace, TraceName } from '../../../../shared/lib/trace'; -import { - EVM_WALLET_TYPE, - WalletClientType, - useMultichainWalletSnapClient, -} from '../../../hooks/accounts/useMultichainWalletSnapClient'; - -type AccountBalance = { - [key: string]: string | number; -}; - -const WalletDetails = () => { - const t = useI18nContext(); - const history = useHistory(); - const dispatch = useDispatch(); - const { id } = useParams(); - const decodedId = decodeURIComponent(id as string); - const walletsWithAccounts = useSelector(getWalletsWithAccounts); - const seedPhraseBackedUp = useSelector(getIsPrimarySeedPhraseBackedUp); - const hdKeyrings = useSelector(getMetaMaskHdKeyrings); - const [srpQuizModalVisible, setSrpQuizModalVisible] = useState(false); - const wallet = walletsWithAccounts[decodedId as AccountWalletId]; - const [accountBalances, setAccountBalances] = useState({}); - const [isModalOpen, setIsModalOpen] = useState(false); - - // Initialize wallet snap clients - const solanaClient = useMultichainWalletSnapClient(WalletClientType.Solana); - ///: BEGIN:ONLY_INCLUDE_IF(bitcoin) - const bitcoinClient = useMultichainWalletSnapClient(WalletClientType.Bitcoin); - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(tron) - const tronClient = useMultichainWalletSnapClient(WalletClientType.Tron); - ///: END:ONLY_INCLUDE_IF - - const totalBalance = useMemo( - () => - Object.values(accountBalances) - .reduce((sum, balance) => sum + Number(balance || 0), 0) - .toString(), - [accountBalances], - ); - - const handleBalanceUpdate = useCallback( - (accountId: string, balance: string | number) => { - setAccountBalances((prev) => ({ - ...prev, - [accountId]: balance, - })); - }, - [], - ); - - const handleAccountClick = (account: { id: string; address: string }) => { - dispatch(setAccountDetailsAddress(account.address)); - history.push(`${ACCOUNT_DETAILS_ROUTE}/${account.address}`); - }; - - const handleBack = () => { - history.goBack(); - }; - - if (!wallet) { - return ( - -
- } - > - {t('walletDetails')} -
- - - {t('walletNotFoundDescription', [id])} - - -
- ); - } - - const keyringId = wallet.id.split(':')[1]; - - const isEntropyWallet = wallet.id.includes('entropy'); - const isFirstHdKeyring = hdKeyrings[0]?.metadata?.id === keyringId; - const shouldShowBackupReminder = !seedPhraseBackedUp && isFirstHdKeyring; - - // Now, wallets are composed of multiple groups, so we have to flatten everything. - const accounts = Object.values(wallet.groups).flatMap( - (group) => group.accounts, - ); - - const handleAddAccount = () => { - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setIsModalOpen(false); - }; - - const handleCreateEthereumAccount = async (): Promise => { - trace({ name: TraceName.AddAccount }); - try { - await dispatch(addNewAccount(keyringId, false)); - return true; - } catch (error) { - console.error('Error creating Ethereum account:', error); - return false; - } finally { - endTrace({ name: TraceName.AddAccount }); - } - }; - - const handleCreateSnapAccount = async ( - clientType: WalletClientType, - chainId: CaipChainId, - ): Promise => { - trace({ name: TraceName.AddAccount }); - try { - let client; - - if (clientType === WalletClientType.Solana) { - client = solanaClient; - } - ///: BEGIN:ONLY_INCLUDE_IF(bitcoin) - else if (clientType === WalletClientType.Bitcoin) { - client = bitcoinClient; - } - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(tron) - else if (clientType === WalletClientType.Tron) { - client = tronClient; - } - ///: END:ONLY_INCLUDE_IF - else { - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31893 - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - console.error(`Unsupported client type: ${clientType}`); - return false; - } - - if (!client) { - console.error(`Client not available for type: ${clientType}`); - return false; - } - - await client.createAccount( - { - scope: chainId, - entropySource: keyringId, - }, - { - displayConfirmation: false, - displayAccountNameSuggestion: false, - setSelectedAccount: false, - }, - ); - return true; - } catch (error) { - console.error(`Error creating ${clientType} account:`, error); - return false; - } finally { - endTrace({ name: TraceName.AddAccount }); - } - }; - - const handleAccountTypeSelect = async ( - accountType: WalletClientType | typeof EVM_WALLET_TYPE, - ) => { - let success = false; - - if (accountType === EVM_WALLET_TYPE) { - success = await handleCreateEthereumAccount(); - } else if (accountType === WalletClientType.Solana) { - success = await handleCreateSnapAccount( - WalletClientType.Solana, - SolScope.Mainnet as CaipChainId, - ); - } - ///: BEGIN:ONLY_INCLUDE_IF(bitcoin) - else if (accountType === WalletClientType.Bitcoin) { - success = await handleCreateSnapAccount( - WalletClientType.Bitcoin, - BtcScope.Mainnet as CaipChainId, - ); - } - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(tron) - else if (accountType === WalletClientType.Tron) { - success = await handleCreateSnapAccount( - WalletClientType.Tron, - TrxScope.Mainnet as CaipChainId, - ); - } - ///: END:ONLY_INCLUDE_IF - - if (success) { - handleCloseModal(); - } - }; - - const rowStylesProps = { - display: Display.Flex, - justifyContent: JustifyContent.spaceBetween, - alignItems: AlignItems.center, - backgroundColor: BackgroundColor.backgroundAlternative, - }; - - return ( - -
- } - > - {t('walletDetails')} -
- - - - - {t('walletName')} - - - {wallet.metadata.name} - - - - - - {t('balance')} - - - - - - {isEntropyWallet ? ( - - { - if (shouldShowBackupReminder) { - const backUpSRPRoute = `${ONBOARDING_REVIEW_SRP_ROUTE}/?isFromReminder=true`; - history.push(backUpSRPRoute); - } else { - setSrpQuizModalVisible(true); - } - }} - > - - - {t('secretRecoveryPhrase')} - - - - {shouldShowBackupReminder ? ( - - {t('backup')} - - ) : null} - - - - - ) : null} - - {accounts.length > 0 && ( - - {accounts.map((account) => ( - - ))} - {isEntropyWallet ? ( - - - - - {t('addAccount')} - - - - ) : null} - - )} - - {isEntropyWallet && srpQuizModalVisible && ( - setSrpQuizModalVisible(false)} - closeAfterCompleting - /> - )} - {isModalOpen && ( - - - - - - - )} -
- ); -}; - -WalletDetails.propTypes = {}; - -export default WalletDetails; diff --git a/ui/pages/multichain-accounts/wallet-details/wallet-details.scss b/ui/pages/multichain-accounts/wallet-details/wallet-details.scss deleted file mode 100644 index ccd254a47f45..000000000000 --- a/ui/pages/multichain-accounts/wallet-details/wallet-details.scss +++ /dev/null @@ -1,27 +0,0 @@ -@use "design-system"; - -.wallet-details-page { - max-width: 600px; - - .wallet-details-page__rows-container { - .wallet-details-page__row { - margin-bottom: 1px; - - &:first-of-type { - border-top-left-radius: 8px; - border-top-right-radius: 8px; - } - - &:last-of-type { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - margin-bottom: 0; - } - - &.wallet-details-page__srp-button { - cursor: pointer; - border: none; - } - } - } -} diff --git a/ui/pages/routes/routes.component.tsx b/ui/pages/routes/routes.component.tsx index 2a2322e91004..1a18ddbc0d01 100644 --- a/ui/pages/routes/routes.component.tsx +++ b/ui/pages/routes/routes.component.tsx @@ -22,9 +22,7 @@ import Loading from '../../components/ui/loading-screen'; import { Modal } from '../../components/app/modals'; import Alert from '../../components/ui/alert'; import { - AccountListMenu, NetworkListMenu, - AccountDetails, ImportNftsModal, ImportTokensModal, } from '../../components/multichain'; @@ -60,9 +58,6 @@ import { DEFI_ROUTE, DEEP_LINK_ROUTE, SMART_ACCOUNT_UPDATE, - WALLET_DETAILS_ROUTE, - ACCOUNT_DETAILS_ROUTE, - ACCOUNT_DETAILS_QR_CODE_ROUTE, ACCOUNT_LIST_PAGE_ROUTE, MULTICHAIN_ACCOUNT_ADDRESS_LIST_PAGE_ROUTE, MULTICHAIN_ACCOUNT_PRIVATE_KEY_LIST_PAGE_ROUTE, @@ -90,7 +85,6 @@ import { oldestPendingConfirmationSelector, getUnapprovedTransactions, getPendingApprovals, - getIsMultichainAccountsState1Enabled, } from '../../selectors'; import { getApprovalFlows } from '../../selectors/approvals'; @@ -147,8 +141,6 @@ import { } from '../../../shared/lib/confirmation.utils'; import { type Confirmation } from '../confirmations/types/confirm'; import { SmartAccountUpdate } from '../confirmations/components/confirm/smart-account-update'; -import { MultichainAccountDetails } from '../multichain-accounts/account-details'; -import { AddressQRCode } from '../multichain-accounts/address-qr-code'; import { MultichainAccountAddressListPage } from '../multichain-accounts/multichain-account-address-list-page'; import { MultichainAccountPrivateKeyListPage } from '../multichain-accounts/multichain-account-private-key-list-page'; import MultichainAccountIntroModalContainer from '../../components/app/modals/multichain-accounts/intro-modal'; @@ -429,12 +421,6 @@ const DeepLink = mmLazy( // TODO: This is a named export. Fix incorrect type casting once `mmLazy` is updated to handle non-default export types. (() => import('../deep-link/deep-link.tsx')) as unknown as DynamicImportType, ); -const WalletDetails = mmLazy( - (() => - import( - '../multichain-accounts/wallet-details/index.ts' - )) as unknown as DynamicImportType, -); const MultichainAccountDetailsPage = mmLazy( (() => @@ -531,9 +517,6 @@ export default function Routes() { const isDeprecatedNetworkModalOpen = useAppSelector( (state) => state.appState.deprecatedNetworkModalOpen, ); - const accountDetailsAddress = useAppSelector( - (state) => state.appState.accountDetailsAddress, - ); const isImportNftsModalOpen = useAppSelector( (state) => state.appState.importNftsModal.open, ); @@ -556,10 +539,6 @@ export default function Routes() { dispatch(hideKeyringRemovalResultModal()); ///: END:ONLY_INCLUDE_IF - const isMultichainAccountsState1Enabled = useAppSelector( - getIsMultichainAccountsState1Enabled, - ); - // Multichain intro modal logic (extracted to custom hook) const { showMultichainIntroModal, setShowMultichainIntroModal } = useMultichainAccountsIntroModal(isUnlocked, location); @@ -1010,27 +989,6 @@ export default function Routes() { exact layout={RootLayout} /> - - - { - if (!accountDetailsAddress || isMultichainAccountsState1Enabled) { - return null; - } - return ; - }; - const loadMessage = loadingMessage ? getConnectingLabel(loadingMessage, { providerType, providerId }, { t }) : null; @@ -1115,16 +1066,11 @@ export default function Routes() { // is already a fullscreen interface. !isShowingDeepLinkRoute; - const accountListMenu = isMultichainAccountsState1Enabled ? ( + const accountListMenu = ( dispatch(toggleAccountMenu())} privacyMode={privacyMode} /> - ) : ( - dispatch(toggleAccountMenu())} - privacyMode={privacyMode} - /> ); const isSidepanel = getEnvironmentType() === ENVIRONMENT_TYPE_SIDEPANEL; @@ -1154,7 +1100,6 @@ export default function Routes() { /> ) : null} - {renderAccountDetails()} {isImportNftsModalOpen ? ( dispatch(hideImportNftsModal())} /> ) : null} diff --git a/ui/pages/routes/utils.js b/ui/pages/routes/utils.js index d874d90e197b..81f27df9c0dc 100644 --- a/ui/pages/routes/utils.js +++ b/ui/pages/routes/utils.js @@ -23,9 +23,6 @@ import { SEND_ROUTE, SNAPS_VIEW_ROUTE, DEEP_LINK_ROUTE, - WALLET_DETAILS_ROUTE, - ACCOUNT_DETAILS_ROUTE, - ACCOUNT_DETAILS_QR_CODE_ROUTE, MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE, SHIELD_PLAN_ROUTE, MULTICHAIN_WALLET_DETAILS_PAGE_ROUTE, @@ -211,16 +208,6 @@ export function hideAppHeader(props) { return true; } - const isWalletDetailsPage = Boolean( - matchPath(location.pathname, { - path: WALLET_DETAILS_ROUTE, - exact: false, - }), - ); - if (isWalletDetailsPage) { - return true; - } - const isSnapsHome = Boolean( matchPath(location.pathname, { path: SNAPS_VIEW_ROUTE, @@ -252,28 +239,6 @@ export function hideAppHeader(props) { return true; } - const isMultichainAccountDetailsPage = Boolean( - matchPath(location.pathname, { - path: ACCOUNT_DETAILS_ROUTE, - exact: false, - }), - ); - - if (isMultichainAccountDetailsPage) { - return true; - } - - const isMultichainAccountDetailsQRCodePage = Boolean( - matchPath(location.pathname, { - path: ACCOUNT_DETAILS_QR_CODE_ROUTE, - exact: false, - }), - ); - - if (isMultichainAccountDetailsQRCodePage) { - return true; - } - const isHandlingAddEthereumChainRequest = Boolean( matchPath(location.pathname, { path: CONFIRMATION_V_NEXT_ROUTE, diff --git a/ui/store/actions.ts b/ui/store/actions.ts index a0010f1698cd..df4d44ab0fe1 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4722,13 +4722,6 @@ export function toggleNetworkMenu(payload?: { }; } -export function setAccountDetailsAddress(address: string) { - return { - type: actionConstants.SET_ACCOUNT_DETAILS_ADDRESS, - payload: address, - }; -} - export function setParticipateInMetaMetrics( participationPreference: boolean, ): ThunkAction< From c93eff7dd0849f753af6801c56b593e368f9d886 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 19 Nov 2025 17:57:22 +0100 Subject: [PATCH 02/11] fix: imports, UTs, restore somes files --- .../multichain-accounts-tree.tsx | 5 +- .../account-details-menu-item.test.js | 31 ++-- .../confirmations/context/confirm/index.tsx | 1 - .../account-type-utils.test.ts | 130 +++++++++++++++ .../account-details/account-type-utils.ts | 149 ++++++++++++++++++ .../account-details/index.ts | 1 + ui/pages/pages.scss | 1 - 7 files changed, 303 insertions(+), 15 deletions(-) create mode 100644 ui/pages/multichain-accounts/account-details/account-type-utils.test.ts create mode 100644 ui/pages/multichain-accounts/account-details/account-type-utils.ts create mode 100644 ui/pages/multichain-accounts/account-details/index.ts diff --git a/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx b/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx index 6b77e9b04ba9..409177000369 100644 --- a/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx +++ b/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx @@ -1,6 +1,5 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { InternalAccount } from '@metamask/keyring-internal-api'; -import { useHistory } from 'react-router-dom'; import { Box, ButtonLink, ButtonLinkSize, Text } from '../../component-library'; import { AlignItems, @@ -44,8 +43,6 @@ export const MultichainAccountsTree = ({ onClose, onAccountTreeItemClick, }: MultichainAccountsTreeProps) => { - const history = useHistory(); - const accountsTree = useMemo(() => { // We keep a flag to check if there are any hidden accounts let hasHiddenAccounts: boolean = false; diff --git a/ui/components/multichain/menu-items/account-details-menu-item.test.js b/ui/components/multichain/menu-items/account-details-menu-item.test.js index 3f11bc9582e1..5195993ba6cb 100644 --- a/ui/components/multichain/menu-items/account-details-menu-item.test.js +++ b/ui/components/multichain/menu-items/account-details-menu-item.test.js @@ -2,12 +2,28 @@ import React from 'react'; import { fireEvent, renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; -import * as actions from '../../../store/actions'; +import { MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE } from '../../../helpers/constants/routes'; import { getSelectedInternalAccountFromMockState } from '../../../../test/jest/mocks'; import { AccountDetailsMenuItem } from '.'; const mockInternalAccount = getSelectedInternalAccountFromMockState(mockState); +const mockHistoryPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +jest.mock('../../../selectors/multichain-accounts/account-tree', () => ({ + ...jest.requireActual( + '../../../selectors/multichain-accounts/account-tree.ts', + ), + getSelectedAccountGroup: () => mockInternalAccount.address, +})); + const render = () => { const store = configureStore(mockState); return renderWithProvider( @@ -20,13 +36,8 @@ const render = () => { ); }; -jest.mock('../../../store/actions', () => ({ - ...jest.requireActual('../../../store/actions.ts'), - setAccountDetailsAddress: jest.fn().mockReturnValue({ type: 'TYPE' }), -})); - describe('AccountDetailsMenuItem', () => { - it('opens the Account Details modal with the correct address', () => { + it('navigates to the multichain account details page with selected account group', () => { global.platform = { openTab: jest.fn() }; const { getByText, getByTestId } = render(); @@ -34,8 +45,10 @@ describe('AccountDetailsMenuItem', () => { fireEvent.click(getByTestId('account-list-menu-details')); - expect(actions.setAccountDetailsAddress).toHaveBeenCalledWith( - mockInternalAccount.address, + expect(mockHistoryPush).toHaveBeenCalledWith( + `${MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE}/${encodeURIComponent( + mockInternalAccount.address, + )}`, ); }); }); diff --git a/ui/pages/confirmations/context/confirm/index.tsx b/ui/pages/confirmations/context/confirm/index.tsx index 844ce37d0b41..928dc482d3cc 100644 --- a/ui/pages/confirmations/context/confirm/index.tsx +++ b/ui/pages/confirmations/context/confirm/index.tsx @@ -36,7 +36,6 @@ export const ConfirmContextProvider: React.FC<{ >(undefined); const { currentConfirmation } = useCurrentConfirmation(); useSyncConfirmPath(currentConfirmation); - const dispatch = useDispatch(); useEffect(() => { setQuoteSelectedForMMSwap(undefined); diff --git a/ui/pages/multichain-accounts/account-details/account-type-utils.test.ts b/ui/pages/multichain-accounts/account-details/account-type-utils.test.ts new file mode 100644 index 000000000000..ff0b88cabd6b --- /dev/null +++ b/ui/pages/multichain-accounts/account-details/account-type-utils.test.ts @@ -0,0 +1,130 @@ +import { InternalAccount } from '@metamask/keyring-internal-api'; +import { + MOCK_ACCOUNT_BIP122_P2WPKH, + MOCK_ACCOUNT_EOA, + MOCK_ACCOUNT_ERC4337, + MOCK_ACCOUNT_HARDWARE, + MOCK_ACCOUNT_INSTITUTIONAL, + MOCK_ACCOUNT_PRIVATE_KEY, + MOCK_ACCOUNT_SOLANA_MAINNET, +} from '../../../../test/data/mock-accounts'; +import { + getAccountTypeCategory, + isEVMAccount, + isSolanaAccount, + isHardwareAccount, + isPrivateKeyAccount, + isInstitutionalEVMAccount, + isBitcoinAccount, +} from './account-type-utils'; + +describe('Account Type Utils', () => { + describe('getAccountTypeCategory', () => { + it('should return "evm" for EOA accounts', () => { + expect(getAccountTypeCategory(MOCK_ACCOUNT_EOA)).toBe('evm'); + }); + + it('should return "evm" for ERC-4337 accounts', () => { + expect(getAccountTypeCategory(MOCK_ACCOUNT_ERC4337)).toBe('evm'); + }); + + it('should return "solana" for Solana accounts', () => { + expect(getAccountTypeCategory(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe( + 'solana', + ); + }); + + it('should return "unknown" for null/undefined accounts', () => { + expect(getAccountTypeCategory(null as unknown as InternalAccount)).toBe( + 'unknown', + ); + expect( + getAccountTypeCategory(undefined as unknown as InternalAccount), + ).toBe('unknown'); + }); + }); + + describe('isEVMAccount', () => { + it('should return true for EOA accounts', () => { + expect(isEVMAccount(MOCK_ACCOUNT_EOA)).toBe(true); + }); + + it('should return true for ERC-4337 accounts', () => { + expect(isEVMAccount(MOCK_ACCOUNT_ERC4337)).toBe(true); + }); + + it('should return false for Solana accounts', () => { + expect(isEVMAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); + }); + }); + + describe('isSolanaAccount', () => { + it('should return true for Solana accounts', () => { + expect(isSolanaAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(true); + }); + + it('should return false for EOA accounts', () => { + expect(isSolanaAccount(MOCK_ACCOUNT_EOA)).toBe(false); + }); + + it('should return false for ERC-4337 accounts', () => { + expect(isSolanaAccount(MOCK_ACCOUNT_ERC4337)).toBe(false); + }); + }); + + describe('isHardwareAccount', () => { + it('should return true for hardware accounts', () => { + expect(isHardwareAccount(MOCK_ACCOUNT_HARDWARE)).toBe(true); + }); + + it('should return false for EOA accounts', () => { + expect(isHardwareAccount(MOCK_ACCOUNT_EOA)).toBe(false); + }); + + it('should return false for Solana accounts', () => { + expect(isHardwareAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); + }); + }); + + describe('isPrivateKeyAccount', () => { + it('should return true for private key accounts', () => { + expect(isPrivateKeyAccount(MOCK_ACCOUNT_PRIVATE_KEY)).toBe(true); + }); + + it('should return false for EOA accounts', () => { + expect(isPrivateKeyAccount(MOCK_ACCOUNT_EOA)).toBe(false); + }); + + it('should return false for Solana accounts', () => { + expect(isPrivateKeyAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); + }); + }); + + describe('isInstitutionalEVMAccount', () => { + it('should return true for institutional EVM accounts', () => { + expect(isInstitutionalEVMAccount(MOCK_ACCOUNT_INSTITUTIONAL)).toBe(true); + }); + + it('should return false for regular EOA accounts', () => { + expect(isInstitutionalEVMAccount(MOCK_ACCOUNT_EOA)).toBe(false); + }); + + it('should return false for regular ERC-4337 accounts', () => { + expect(isInstitutionalEVMAccount(MOCK_ACCOUNT_ERC4337)).toBe(false); + }); + }); + + describe('isBitcoinAccount', () => { + it('should return true for Bitcoin accounts', () => { + expect(isBitcoinAccount(MOCK_ACCOUNT_BIP122_P2WPKH)).toBe(true); + }); + + it('should return false for EOA accounts', () => { + expect(isBitcoinAccount(MOCK_ACCOUNT_EOA)).toBe(false); + }); + + it('should return false for Solana accounts', () => { + expect(isBitcoinAccount(MOCK_ACCOUNT_SOLANA_MAINNET)).toBe(false); + }); + }); +}); diff --git a/ui/pages/multichain-accounts/account-details/account-type-utils.ts b/ui/pages/multichain-accounts/account-details/account-type-utils.ts new file mode 100644 index 000000000000..87e462b729bf --- /dev/null +++ b/ui/pages/multichain-accounts/account-details/account-type-utils.ts @@ -0,0 +1,149 @@ +import { + BtcAccountType, + SolAccountType, + TrxAccountType, + isEvmAccountType, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { InternalAccount } from '@metamask/keyring-internal-api'; + +export type AccountTypeCategory = + | 'evm' + | 'solana' + | 'hardware' + | 'private-key' + | 'institutional-evm' + | 'bitcoin' + | 'tron' + | 'unknown'; + +/** + * Determines the account type category based on the account's type and keyring information + * + * @param account + */ +export const getAccountTypeCategory = ( + account: InternalAccount, +): AccountTypeCategory => { + if (!account) { + return 'unknown'; + } + + const { type, metadata } = account; + const keyringType = metadata?.keyring?.type as KeyringTypes; + const snapId = metadata?.snap?.id; + + // Hardware accounts (must be checked before EVM check) + if ( + keyringType && + [ + KeyringTypes.ledger, + KeyringTypes.trezor, + KeyringTypes.oneKey, + KeyringTypes.lattice, + KeyringTypes.qr, + ].includes(keyringType) + ) { + return 'hardware'; + } + + // Private key accounts (must be checked before EVM check) + if (keyringType === KeyringTypes.simple) { + return 'private-key'; + } + + // Institutional-EVM accounts (must be checked before EVM check) + if ( + keyringType === KeyringTypes.snap && + snapId === 'npm:@metamask/institutional-wallet-snap' + ) { + return 'institutional-evm'; + } + + // EVM accounts (EOA and ERC-4337) - general fallback + if (isEvmAccountType(type)) { + return 'evm'; + } + + // Solana accounts + if (type === SolAccountType.DataAccount) { + return 'solana'; + } + + // Bitcoin accounts + if (Object.values(BtcAccountType).includes(type as BtcAccountType)) { + return 'bitcoin'; + } + + // TRON accounts + if (type === TrxAccountType.Eoa) { + return 'tron'; + } + + return 'unknown'; +}; + +/** + * Checks if an account is an EVM account (EOA or ERC-4337) + * + * @param account - The internal account object to check. + */ +export const isEVMAccount = (account: InternalAccount): boolean => { + return getAccountTypeCategory(account) === 'evm'; +}; + +/** + * Checks if an account is a Solana account + * + * @param account - The internal account object to check. + */ +export const isSolanaAccount = (account: InternalAccount): boolean => { + return getAccountTypeCategory(account) === 'solana'; +}; + +/** + * Checks if an account is a hardware wallet account + * + * @param account - The internal account object to check. + */ +export const isHardwareAccount = (account: InternalAccount): boolean => { + return getAccountTypeCategory(account) === 'hardware'; +}; + +/** + * Checks if an account is a private key account + * + * @param account - The internal account object to check. + */ +export const isPrivateKeyAccount = (account: InternalAccount): boolean => { + return getAccountTypeCategory(account) === 'private-key'; +}; + +/** + * Checks if an account is an institutional EVM account + * + * @param account - The internal account object to check. + */ +export const isInstitutionalEVMAccount = ( + account: InternalAccount, +): boolean => { + return getAccountTypeCategory(account) === 'institutional-evm'; +}; + +/** + * Checks if an account is a Bitcoin account + * + * @param account - The internal account object to check. + */ +export const isBitcoinAccount = (account: InternalAccount): boolean => { + return getAccountTypeCategory(account) === 'bitcoin'; +}; + +/** + * Checks if an account is a Tron account + * + * @param account - The internal account object to check. + */ +export const isTronAccount = (account: InternalAccount): boolean => { + return getAccountTypeCategory(account) === 'tron'; +}; diff --git a/ui/pages/multichain-accounts/account-details/index.ts b/ui/pages/multichain-accounts/account-details/index.ts new file mode 100644 index 000000000000..83fa94193d04 --- /dev/null +++ b/ui/pages/multichain-accounts/account-details/index.ts @@ -0,0 +1 @@ +export * from './account-type-utils'; diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 6e5250f71671..fa00f986dccc 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -26,7 +26,6 @@ @import 'bridge/index'; @import 'unlock-page/index'; @import 'multichain-accounts/account-list/account-list'; -@import 'multichain-accounts/wallet-details/wallet-details'; @import 'multichain-accounts/wallet-details-page/index'; @import 'multi-srp/import-srp/index'; @import 'multichain-accounts/address-qr-code/address-qr-code'; From 2229dc28ceb1514f7b98776d0cd072e70933ae15 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 19 Nov 2025 18:03:31 +0100 Subject: [PATCH 03/11] fix: remove old imports --- ui/pages/pages.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index fa00f986dccc..20e7c623171a 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -28,8 +28,6 @@ @import 'multichain-accounts/account-list/account-list'; @import 'multichain-accounts/wallet-details-page/index'; @import 'multi-srp/import-srp/index'; -@import 'multichain-accounts/address-qr-code/address-qr-code'; -@import 'multichain-accounts/base-account-details/base-account-details'; @import 'multichain-accounts/multichain-account-details-page/index'; @import 'shield-plan/index'; @import 'multichain-accounts/multichain-accounts-connect-page/index'; From 7d3e4879a9e999bda61e229703b584f044fb1b44 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 19 Nov 2025 21:29:46 +0100 Subject: [PATCH 04/11] fix: vault corruption E2E --- .../pages/multichain/address-list-modal.ts | 14 ++++++++++++++ .../multichain-account-details-page.ts | 18 ++++++++++++++++++ .../vault-corruption/vault-corruption.spec.ts | 14 ++++++++++---- .../confirmations/context/confirm/index.tsx | 1 - .../multichain-account-details-page.test.tsx | 8 +------- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/test/e2e/page-objects/pages/multichain/address-list-modal.ts b/test/e2e/page-objects/pages/multichain/address-list-modal.ts index ba2b0d925129..c3dcc2dbbe17 100644 --- a/test/e2e/page-objects/pages/multichain/address-list-modal.ts +++ b/test/e2e/page-objects/pages/multichain/address-list-modal.ts @@ -3,6 +3,9 @@ import { Driver } from '../../../webdriver/driver'; class AddressListModal { private driver: Driver; + private readonly accountAddress = + '[data-testid="multichain-address-row-address"]'; + private readonly copyButton = '[data-testid="multichain-address-row-copy-button"]'; @@ -62,6 +65,17 @@ class AddressListModal { await qrButton.click(); } + async getTruncatedAccountAddress(addressIndex: number = 0): Promise { + console.log('Get truncated account address'); + const addressElements = await this.driver.findElements(this.accountAddress); + if (addressIndex < 0 || addressIndex >= addressElements.length) { + throw new Error('Invalid account row index'); + } + const addressElement = addressElements[addressIndex]; + const address = await addressElement.getText(); + return address; + } + async goBack(): Promise { await this.driver.clickElement(this.backButton); } diff --git a/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts b/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts index 9fcb8690462a..57089fc42b57 100644 --- a/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts +++ b/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts @@ -155,6 +155,24 @@ class MultichainAccountDetailsPage { await netoworksRow.click(); } + async getTruncatedAccountAddressFromNetworksPage(options: { + accountRowIndex: number; + }): Promise { + console.log( + 'Get truncated account address from account details > networks page', + ); + const { accountRowIndex } = options; + const accountRows = await this.driver.findElements( + '[data-testid="multichain-address-row-address"]', + ); + if (accountRowIndex < 0 || accountRowIndex >= accountRows.length) { + throw new Error('Invalid account row index'); + } + const accountRow = accountRows[accountRowIndex]; + const address = await accountRow.getText(); + return address; + } + /** * Click on the private key row */ diff --git a/test/e2e/tests/vault-corruption/vault-corruption.spec.ts b/test/e2e/tests/vault-corruption/vault-corruption.spec.ts index 9a22b7f27bc2..5260bfb074b9 100644 --- a/test/e2e/tests/vault-corruption/vault-corruption.spec.ts +++ b/test/e2e/tests/vault-corruption/vault-corruption.spec.ts @@ -8,8 +8,9 @@ import { import HomePage from '../../page-objects/pages/home/homepage'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import AccountListPage from '../../page-objects/pages/account-list-page'; -import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import LoginPage from '../../page-objects/pages/login-page'; +import MultichainAccountDetailsPage from '../../page-objects/pages/multichain/multichain-account-details-page'; +import AddressListModal from '../../page-objects/pages/multichain/address-list-modal'; describe('Vault Corruption', function () { this.timeout(120000); // This test is very long, so we need an unusually high timeout @@ -257,10 +258,15 @@ describe('Vault Corruption', function () { await accountListPage.checkPageIsLoaded(); await accountListPage.openAccountDetailsModal('Account 1'); - const accountDetailsModal = new AccountDetailsModal(driver); - await accountDetailsModal.checkPageIsLoaded(); + const accountDetailsPage = new MultichainAccountDetailsPage(driver); + await accountDetailsPage.checkPageIsLoaded(); + await accountDetailsPage.clickNetworksRow(); + + const addressListModal = new AddressListModal(driver); + const accountAddress = await addressListModal.getTruncatedAccountAddress(0); + await addressListModal.goBack(); + await accountDetailsPage.navigateBack(); - const accountAddress = await accountDetailsModal.getAccountAddress(); return accountAddress; } diff --git a/ui/pages/confirmations/context/confirm/index.tsx b/ui/pages/confirmations/context/confirm/index.tsx index 928dc482d3cc..90dc3207b87f 100644 --- a/ui/pages/confirmations/context/confirm/index.tsx +++ b/ui/pages/confirmations/context/confirm/index.tsx @@ -7,7 +7,6 @@ import React, { useState, } from 'react'; import { QuoteResponse } from '@metamask/bridge-controller'; -import { useDispatch } from 'react-redux'; import useCurrentConfirmation from '../../hooks/useCurrentConfirmation'; import useSyncConfirmPath from '../../hooks/useSyncConfirmPath'; diff --git a/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.test.tsx b/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.test.tsx index d5b45162aca7..368ae5b0604a 100644 --- a/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.test.tsx +++ b/ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.test.tsx @@ -208,15 +208,9 @@ describe('MultichainAccountDetailsPage', () => { fireEvent.click(removeButton); // Verify that dispatch was called with removeAccount action - expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledTimes(1); // First call should be the removeAccount thunk (AsyncFunction) expect(mockDispatch).toHaveBeenNthCalledWith(1, expect.any(Function)); - - // Second call should be setAccountDetailsAddress action - expect(mockDispatch).toHaveBeenNthCalledWith(2, { - type: 'SET_ACCOUNT_DETAILS_ADDRESS', - payload: '', - }); }); }); From 9cb92a961dd213b735b145772af5288c110f24b1 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 19 Nov 2025 22:42:33 +0100 Subject: [PATCH 05/11] fix: update locales + remove unused bits --- app/_locales/de/messages.json | 10 ---------- app/_locales/el/messages.json | 10 ---------- app/_locales/en/messages.json | 10 ---------- app/_locales/en_GB/messages.json | 10 ---------- app/_locales/es/messages.json | 10 ---------- app/_locales/fr/messages.json | 10 ---------- app/_locales/ga/messages.json | 10 ---------- app/_locales/hi/messages.json | 10 ---------- app/_locales/id/messages.json | 10 ---------- app/_locales/ja/messages.json | 10 ---------- app/_locales/ko/messages.json | 10 ---------- app/_locales/pt/messages.json | 10 ---------- app/_locales/ru/messages.json | 10 ---------- app/_locales/tl/messages.json | 10 ---------- app/_locales/tr/messages.json | 10 ---------- app/_locales/vi/messages.json | 10 ---------- app/_locales/zh_CN/messages.json | 10 ---------- .../multichain/account-details/account-details.test.js | 3 --- .../multichain/global-menu/global-menu.test.tsx | 5 ----- .../smart-account-update/smart-account-update.test.tsx | 1 - .../smart-account-update-splash.test.tsx | 1 - .../components/confirm/title/title.test.tsx | 1 - .../confirmations/hooks/useSmartAccountActions.test.ts | 1 - ui/pages/home/home.component.stories.tsx | 1 - 24 files changed, 183 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index e0a5f22e9e0a..fcec94fb0f67 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "unsere Hardware-Wallet-Verbindungsanleitung" }, - "walletDetails": { - "message": "Wallet-Details" - }, "walletName": { "message": "Wallet-Name" }, - "walletNotFoundDescription": { - "message": "Die Wallet mit der ID $1 wurde nicht gefunden.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Wallet nicht gefunden" - }, "walletReadyLearn": { "message": "$1 Sie können diese Phrase sicher aufbewahren, damit Sie nie den Zugriff auf Ihr Geld verlieren.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 3922fb16a015..05b3b60f7997 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "ο οδηγός μας σύνδεσης πορτοφολιού υλικού" }, - "walletDetails": { - "message": "Λεπτομέρειες πορτοφολιού" - }, "walletName": { "message": "Όνομα πορτοφολιού" }, - "walletNotFoundDescription": { - "message": "Το πορτοφόλι με αρ. $1 δεν βρέθηκε.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Το πορτοφόλι δεν βρέθηκε" - }, "walletReadyLearn": { "message": "$1 να φυλάξετε σε ασφαλές μέρος τη μυστική φράση για να διασφαλίσετε την πρόσβαση στα χρήματά σας.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 53ef42e3dd34..e0ac44585a12 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -8422,19 +8422,9 @@ "walletConnectionGuide": { "message": "our hardware wallet connection guide" }, - "walletDetails": { - "message": "Wallet details" - }, "walletName": { "message": "Wallet name" }, - "walletNotFoundDescription": { - "message": "The wallet with ID $1 was not found.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Wallet not found" - }, "walletReadyLearn": { "message": "$1 you can keep this phrase safe so you never lose access to your money.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 53ef42e3dd34..e0ac44585a12 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -8422,19 +8422,9 @@ "walletConnectionGuide": { "message": "our hardware wallet connection guide" }, - "walletDetails": { - "message": "Wallet details" - }, "walletName": { "message": "Wallet name" }, - "walletNotFoundDescription": { - "message": "The wallet with ID $1 was not found.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Wallet not found" - }, "walletReadyLearn": { "message": "$1 you can keep this phrase safe so you never lose access to your money.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 644a7838ad70..966806b71470 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "nuestra guía de conexión del monedero físico" }, - "walletDetails": { - "message": "Detalles de la billetera" - }, "walletName": { "message": "Nombre de la billetera" }, - "walletNotFoundDescription": { - "message": "No se encontró la billetera con ID $1.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "No se encontró la billetera" - }, "walletReadyLearn": { "message": "$1 puedes guardar esta frase de forma segura para nunca perder el acceso a tu dinero.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index a3d50dd4de49..d9aa7b4f5dcb 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "notre guide de connexion des portefeuilles matériels" }, - "walletDetails": { - "message": "Détails du portefeuille" - }, "walletName": { "message": "Nom du portefeuille" }, - "walletNotFoundDescription": { - "message": "Le portefeuille avec l’identifiant $1 n’a pas été trouvé.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Portefeuille non trouvé" - }, "walletReadyLearn": { "message": "$1 conservez cette phrase en lieu sûr pour ne jamais perdre l’accès à votre argent.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/ga/messages.json b/app/_locales/ga/messages.json index a6a5b29b2356..85600956d437 100644 --- a/app/_locales/ga/messages.json +++ b/app/_locales/ga/messages.json @@ -7626,19 +7626,9 @@ "walletConnectionGuide": { "message": "ár dtreoir nasctha sparán crua-earraí" }, - "walletDetails": { - "message": "Sonraí sparán" - }, "walletName": { "message": "Ainm an sparáin" }, - "walletNotFoundDescription": { - "message": "Níor aimsíodh an sparán leis an ID $1.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Níor aimsíodh an sparán" - }, "walletReadyLearn": { "message": "$1 is féidir leat an frása seo a choinneáil slán ionas nach gcaillfidh tú rochtain ar do chuid airgid choíche.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9ed0263ca2ad..2b2f43086d46 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -7006,19 +7006,9 @@ "walletConnectionGuide": { "message": "हमारी hardware wallet कनेक्शन गाइड" }, - "walletDetails": { - "message": "वॉलेट विवरण" - }, "walletName": { "message": "वॉलेट का नाम" }, - "walletNotFoundDescription": { - "message": "$1 ID वाला वॉलेट नहीं मिला।", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "वॉलेट नहीं मिला" - }, "walletReadyLearn": { "message": "$1 आप इस फ्रेज़ को सुरक्षित रख सकते हैं ताकि आप कभी भी अपने पैसे तक पहुंच न खोएं।", "description": "$1 is the link to Learn how" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 57800b7c7eaf..fcc3bf90b553 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "panduan koneksi dompet perangkat keras kami" }, - "walletDetails": { - "message": "Detail dompet" - }, "walletName": { "message": "Nama dompet" }, - "walletNotFoundDescription": { - "message": "Dompet dengan ID $1 tidak ditemukan.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Dompet tidak ditemukan" - }, "walletReadyLearn": { "message": "$1 Anda dapat menyimpan frasa ini dengan aman sehingga Anda tidak akan pernah kehilangan akses ke uang Anda.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 4337a86260cd..e21d742fff0f 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "弊社のハードウェアウォレット接続ガイド" }, - "walletDetails": { - "message": "ウォレットの詳細" - }, "walletName": { "message": "ウォレット名" }, - "walletNotFoundDescription": { - "message": "IDが$1のウォレットは見つかりませんでした。", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "ウォレットが見つかりません" - }, "walletReadyLearn": { "message": "$1 ご自身の資金にアクセスできなくなることを防ぐため、このフレーズは安全に保管しましょう。", "description": "$1 is the link to Learn how" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 06fe426381db..ec8b96b14dc2 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "당사의 하드웨어 지갑 연결 가이드" }, - "walletDetails": { - "message": "지갑 세부 정보" - }, "walletName": { "message": "지갑 이름" }, - "walletNotFoundDescription": { - "message": "ID가 $1인 지갑을 찾을 수 없습니다.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "지갑을 찾을 수 없음" - }, "walletReadyLearn": { "message": "$1 이 구문을 안전하게 보관하면 자산 접근 권한을 잃지 않을 수 있습니다.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 953bfce3506f..d50ef58d8b07 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "nosso guia de conexão com a carteira de hardware" }, - "walletDetails": { - "message": "Detalhes da carteira" - }, "walletName": { "message": "Nome da carteira" }, - "walletNotFoundDescription": { - "message": "A carteira com o ID $1 não foi encontrada.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Carteira não encontrada" - }, "walletReadyLearn": { "message": "$1 você pode guardar esta frase em segurança para nunca perder o acesso ao seu dinheiro.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b5dc3bf4ad57..901c1f025942 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "наше руководство по подключению аппаратного кошелька" }, - "walletDetails": { - "message": "Реквизиты кошелька" - }, "walletName": { "message": "Имя кошелька" }, - "walletNotFoundDescription": { - "message": "Кошелек с идентификатором $1 не найден.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Кошелек не найден" - }, "walletReadyLearn": { "message": "$1 Вы можете сохранить эту фразу в надежном месте, чтобы никогда не потерять доступ к своим деньгам.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index d79ac472a599..7bd4d5563efa 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "ang aming gabay sa pagkonekta ng wallet na hardware" }, - "walletDetails": { - "message": "Mga detalye ng wallet" - }, "walletName": { "message": "Pangalan ng wallet" }, - "walletNotFoundDescription": { - "message": "Hindi nahanap ang wallet na may ID na $1.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Hindi nahanap ang wallet" - }, "walletReadyLearn": { "message": "$1 maaari mong panatiling ligtas ang pariralang ito nang sa gayon hindi mawawala ang access mo sa iyong pera.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index ddec7ab9b0ca..238051a25124 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "donanım cüzdanı bağlantı kılavuzumuz" }, - "walletDetails": { - "message": "Cüzdan bilgileri" - }, "walletName": { "message": "Cüzdan adı" }, - "walletNotFoundDescription": { - "message": "$1 kimliği ile cüzdan bulunamadı.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Cüzdan bulunamadı" - }, "walletReadyLearn": { "message": "$1 paranıza erişimi asla kaybetmemeniz için bu ifadeyi güvenli bir şekilde saklayabilirsiniz.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 31c97bee89e3..3583ea88692f 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "hướng dẫn của chúng tôi về cách kết nối ví cứng" }, - "walletDetails": { - "message": "Chi tiết ví" - }, "walletName": { "message": "Tên ví" }, - "walletNotFoundDescription": { - "message": "Không tìm thấy ví có ID $1.", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "Không tìm thấy ví" - }, "walletReadyLearn": { "message": "$1 bạn có thể lưu giữ cụm từ này ở nơi an toàn để không bao giờ mất quyền truy cập vào tiền của mình.", "description": "$1 is the link to Learn how" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index e881cf909d14..bc643386c192 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -7012,19 +7012,9 @@ "walletConnectionGuide": { "message": "我们的硬件钱包连接指南" }, - "walletDetails": { - "message": "钱包详情" - }, "walletName": { "message": "钱包名称" }, - "walletNotFoundDescription": { - "message": "未找到 ID 为 $1 的钱包。", - "description": "$1 is the wallet ID" - }, - "walletNotFoundTitle": { - "message": "未找到钱包" - }, "walletReadyLearn": { "message": "$1 您需要妥善保管该助记词,这样永远不会失去对您的资金的访问权限。", "description": "$1 is the link to Learn how" diff --git a/ui/components/multichain/account-details/account-details.test.js b/ui/components/multichain/account-details/account-details.test.js index 0bf1d5f76f7a..f520bfd4d884 100644 --- a/ui/components/multichain/account-details/account-details.test.js +++ b/ui/components/multichain/account-details/account-details.test.js @@ -10,7 +10,6 @@ import { clearAccountDetails, exportAccount, hideWarning, - setAccountDetailsAddress, } from '../../../store/actions'; import configureStore from '../../../store/store'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; @@ -28,7 +27,6 @@ describe('AccountDetails', () => { mockState.metamask.internalAccounts.accounts, )[0]; const { address } = account; - const mockSetAccountDetailsAddress = jest.fn(); const mockClearAccountDetails = jest.fn(); const mockExportAccount = jest.fn().mockResolvedValue(true); const mockHideWarning = jest.fn(); @@ -37,7 +35,6 @@ describe('AccountDetails', () => { clearAccountDetails.mockReturnValue(mockClearAccountDetails); exportAccount.mockReturnValue(mockExportAccount); hideWarning.mockReturnValue(mockHideWarning); - setAccountDetailsAddress.mockReturnValue(mockSetAccountDetailsAddress); }); afterEach(() => jest.clearAllMocks()); diff --git a/ui/components/multichain/global-menu/global-menu.test.tsx b/ui/components/multichain/global-menu/global-menu.test.tsx index a7ef53a1139e..87c09ce23708 100644 --- a/ui/components/multichain/global-menu/global-menu.test.tsx +++ b/ui/components/multichain/global-menu/global-menu.test.tsx @@ -47,11 +47,6 @@ jest.mock('react-router-dom-v5-compat', () => ({ })); const mockLockMetaMask = jest.fn(); -const mockSetAccountDetailsAddress = jest.fn(); -jest.mock('../../../store/actions', () => ({ - lockMetamask: () => mockLockMetaMask, - setAccountDetailsAddress: () => mockSetAccountDetailsAddress, -})); jest.mock('../../../../shared/modules/environment'); diff --git a/ui/pages/confirmations/components/confirm/smart-account-update/smart-account-update.test.tsx b/ui/pages/confirmations/components/confirm/smart-account-update/smart-account-update.test.tsx index b03d1052f5eb..1d9a478c4e3f 100644 --- a/ui/pages/confirmations/components/confirm/smart-account-update/smart-account-update.test.tsx +++ b/ui/pages/confirmations/components/confirm/smart-account-update/smart-account-update.test.tsx @@ -25,7 +25,6 @@ jest.mock('../../../../../hooks/useMultiPolling', () => ({ })); jest.mock('../../../../../store/actions', () => ({ - setAccountDetailsAddress: jest.fn(), setSmartAccountOptIn: jest.fn(), })); diff --git a/ui/pages/confirmations/components/confirm/splash/smart-account-update-splash/smart-account-update-splash.test.tsx b/ui/pages/confirmations/components/confirm/splash/smart-account-update-splash/smart-account-update-splash.test.tsx index 9eb7e3d35f4d..f5e0440d02c9 100644 --- a/ui/pages/confirmations/components/confirm/splash/smart-account-update-splash/smart-account-update-splash.test.tsx +++ b/ui/pages/confirmations/components/confirm/splash/smart-account-update-splash/smart-account-update-splash.test.tsx @@ -29,7 +29,6 @@ jest.mock('../../../../../../hooks/useMultiPolling', () => ({ })); jest.mock('../../../../../../store/actions', () => ({ - setAccountDetailsAddress: jest.fn(), rejectPendingApproval: jest.fn().mockReturnValue({}), setSmartAccountOptIn: jest.fn(), })); diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index b5bda461fc7f..098077ad1d98 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -50,7 +50,6 @@ jest.mock('../info/approve/hooks/use-is-nft', () => ({ jest.mock('../../../../../store/actions', () => ({ getContractMethodData: jest.fn().mockReturnValue({ type: 'dummy' }), - setAccountDetailsAddress: jest.fn().mockReturnValue({ type: 'dummy' }), })); describe('ConfirmTitle', () => { diff --git a/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts b/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts index a5eccbd1519d..522c05554d6f 100644 --- a/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts +++ b/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts @@ -8,7 +8,6 @@ import { useSmartAccountActions } from './useSmartAccountActions'; jest.mock('../../../store/actions', () => ({ rejectPendingApproval: jest.fn().mockReturnValue({}), - setAccountDetailsAddress: jest.fn(), })); const mockDispatch = jest.fn(); diff --git a/ui/pages/home/home.component.stories.tsx b/ui/pages/home/home.component.stories.tsx index 0f231dfd9f94..3c5afb3bd618 100644 --- a/ui/pages/home/home.component.stories.tsx +++ b/ui/pages/home/home.component.stories.tsx @@ -100,7 +100,6 @@ const meta: Meta = { setBasicFunctionalityModalOpen: () => {}, fetchBuyableChains: () => {}, clearRedirectAfterDefaultPage: () => {}, - setAccountDetailsAddress: () => {}, lookupSelectedNetworks: () => {}, }, }; From be7d6840292ecc7d8104867135c69ee71c4d75b9 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 20 Nov 2025 09:52:30 +0100 Subject: [PATCH 06/11] fix: UT + flask E2E --- test/e2e/flask/multi-srp/import-srp.spec.ts | 7 ++++++- .../pages/multichain/multichain-account-details-page.ts | 9 +++++++++ .../multichain/global-menu/global-menu.test.tsx | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/test/e2e/flask/multi-srp/import-srp.spec.ts b/test/e2e/flask/multi-srp/import-srp.spec.ts index d5f4b2e563b0..c2ef22b289b3 100644 --- a/test/e2e/flask/multi-srp/import-srp.spec.ts +++ b/test/e2e/flask/multi-srp/import-srp.spec.ts @@ -14,6 +14,7 @@ import { mockActiveNetworks, withMultiSrp, } from './common-multi-srp'; +import MultichainAccountDetailsPage from '../../page-objects/pages/multichain/multichain-account-details-page'; const TEST_SRP_WORDS_FOR_UI_TEST = [ 'ghost', @@ -56,7 +57,11 @@ describe('Multi SRP - Import SRP', function (this: Suite) { const accountListPage = new AccountListPage(driver); await accountListPage.checkPageIsLoaded(); - await accountListPage.startExportSrpForAccount('Account 2'); + await accountListPage.openAccountDetailsModal('Account 2'); + + const accountDetailsPage = new MultichainAccountDetailsPage(driver); + await accountDetailsPage.checkPageIsLoaded(); + await accountDetailsPage.clickSecretRecoveryPhraseRow(); const privacySettings = new PrivacySettings(driver); await privacySettings.completeRevealSrpQuiz(); diff --git a/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts b/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts index 57089fc42b57..0ae30e3fb804 100644 --- a/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts +++ b/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts @@ -41,6 +41,9 @@ class MultichainAccountDetailsPage { // Account-specific features private readonly showSrpButton = '[data-testid="account-show-srp-button"]'; + private readonly secretRecoveryPhraseRow = + '[data-testid="multichain-srp-backup"]'; + private readonly showPrivateKeyButton = '[data-testid="account-show-private-key-button"]'; @@ -251,6 +254,12 @@ class MultichainAccountDetailsPage { await this.driver.delay(largeDelayMs); } + async clickSecretRecoveryPhraseRow(): Promise { + console.log('Click on the Secret Recovery Phrase row'); + await this.driver.clickElement(this.secretRecoveryPhraseRow); + await this.driver.delay(largeDelayMs); + } + /** * Navigate back from account details */ diff --git a/ui/components/multichain/global-menu/global-menu.test.tsx b/ui/components/multichain/global-menu/global-menu.test.tsx index 87c09ce23708..ce6b68ca7899 100644 --- a/ui/components/multichain/global-menu/global-menu.test.tsx +++ b/ui/components/multichain/global-menu/global-menu.test.tsx @@ -48,6 +48,10 @@ jest.mock('react-router-dom-v5-compat', () => ({ const mockLockMetaMask = jest.fn(); +jest.mock('../../../store/actions', () => ({ + lockMetamask: () => mockLockMetaMask, +})); + jest.mock('../../../../shared/modules/environment'); jest.mock('../../../hooks/useSidePanelEnabled', () => ({ From ea9a4c2b077cc2574ce39cc94b5cda18dd53efc1 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 20 Nov 2025 10:33:24 +0100 Subject: [PATCH 07/11] fix: UT & lint issues --- package.json | 2 +- test/e2e/flask/multi-srp/import-srp.spec.ts | 2 +- .../menu-items/account-details-menu-item.test.js | 12 +++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 755e2421eadb..0f861c128da5 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "test:e2e:firefox": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts", "test:e2e:firefox:dist": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts --dist", "test:e2e:firefox:flask": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts --build-type flask", - "test:e2e:single": "node test/e2e/run-e2e-test.js", + "test:e2e:single": "SELENIUM_BROWSER=firefox node test/e2e/run-e2e-test.js", "ganache:start": "./development/run-ganache.sh", "sentry:publish": "node ./development/sentry-publish.js", "lint": "yarn lint:prettier && yarn lint:eslint && yarn lint:tsc && yarn lint:styles && yarn lint:images", diff --git a/test/e2e/flask/multi-srp/import-srp.spec.ts b/test/e2e/flask/multi-srp/import-srp.spec.ts index c2ef22b289b3..a54deb410fd3 100644 --- a/test/e2e/flask/multi-srp/import-srp.spec.ts +++ b/test/e2e/flask/multi-srp/import-srp.spec.ts @@ -8,13 +8,13 @@ import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow' import HeaderNavbar from '../../page-objects/pages/header-navbar'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HomePage from '../../page-objects/pages/home/homepage'; +import MultichainAccountDetailsPage from '../../page-objects/pages/multichain/multichain-account-details-page'; import PrivacySettings from '../../page-objects/pages/settings/privacy-settings'; import { SECOND_TEST_E2E_SRP, mockActiveNetworks, withMultiSrp, } from './common-multi-srp'; -import MultichainAccountDetailsPage from '../../page-objects/pages/multichain/multichain-account-details-page'; const TEST_SRP_WORDS_FOR_UI_TEST = [ 'ghost', diff --git a/ui/components/multichain/menu-items/account-details-menu-item.test.js b/ui/components/multichain/menu-items/account-details-menu-item.test.js index 8aafba1868ff..6fc2a317cec8 100644 --- a/ui/components/multichain/menu-items/account-details-menu-item.test.js +++ b/ui/components/multichain/menu-items/account-details-menu-item.test.js @@ -9,13 +9,11 @@ import { AccountDetailsMenuItem } from '.'; const mockInternalAccount = getSelectedInternalAccountFromMockState(mockState); -const mockHistoryPush = jest.fn(); +const mockNavigate = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - push: mockHistoryPush, - }), +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useNavigate: () => mockNavigate, })); jest.mock('../../../selectors/multichain-accounts/account-tree', () => ({ @@ -46,7 +44,7 @@ describe('AccountDetailsMenuItem', () => { fireEvent.click(getByTestId('account-list-menu-details')); - expect(mockHistoryPush).toHaveBeenCalledWith( + expect(mockNavigate).toHaveBeenCalledWith( `${MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE}/${encodeURIComponent( mockInternalAccount.address, )}`, From 3c2111e8d9a193958c9721e20b961f7ca5409152 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 20 Nov 2025 10:50:23 +0100 Subject: [PATCH 08/11] fix: UT --- ui/pages/confirmations/hooks/useSmartAccountActions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts b/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts index 522c05554d6f..cd19a965841d 100644 --- a/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts +++ b/ui/pages/confirmations/hooks/useSmartAccountActions.test.ts @@ -34,7 +34,7 @@ describe('useSmartAccountActions', () => { result.current.handleRejectUpgrade(); await flushPromises(); expect(rejectPendingApproval).toHaveBeenCalledTimes(1); - expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledTimes(1); }); }); }); From 13c0a63f06110611e6b69512cffd6603fc3dce4f Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Tue, 25 Nov 2025 13:59:54 +0100 Subject: [PATCH 09/11] fix: package.json + remove forceBip44Version in one test suite --- package.json | 2 +- test/e2e/tests/multichain-accounts/account-details.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 4e9f6d09ddca..3ac2aad73cc4 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "test:e2e:firefox": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts", "test:e2e:firefox:dist": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts --dist", "test:e2e:firefox:flask": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts --build-type flask", - "test:e2e:single": "SELENIUM_BROWSER=firefox node test/e2e/run-e2e-test.js", + "test:e2e:single": "node test/e2e/run-e2e-test.js", "ganache:start": "./development/run-ganache.sh", "sentry:publish": "node ./development/sentry-publish.js", "lint": "yarn lint:prettier && yarn lint:eslint && yarn lint:tsc && yarn lint:styles && yarn lint:images", diff --git a/test/e2e/tests/multichain-accounts/account-details.spec.ts b/test/e2e/tests/multichain-accounts/account-details.spec.ts index 6251896e95b1..993de3094598 100644 --- a/test/e2e/tests/multichain-accounts/account-details.spec.ts +++ b/test/e2e/tests/multichain-accounts/account-details.spec.ts @@ -128,7 +128,6 @@ describe('Multichain Accounts - Account Details', function (this: Suite) { { fixtures: new FixtureBuilder().build(), title: this.test?.fullTitle(), - forceBip44Version: 2, }, async ({ driver }) => { await loginWithoutBalanceValidation(driver); From b8c1be844fe7678f7b176d4c41d2bcb042b1efb3 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 26 Nov 2025 12:17:17 +0100 Subject: [PATCH 10/11] fix: remove unused code --- .../multichain-account-details-page.ts | 18 ------------------ ui/ducks/app/app.ts | 7 ------- ui/store/actionConstants.ts | 1 - 3 files changed, 26 deletions(-) diff --git a/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts b/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts index 0ae30e3fb804..de790a9e0f28 100644 --- a/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts +++ b/test/e2e/page-objects/pages/multichain/multichain-account-details-page.ts @@ -158,24 +158,6 @@ class MultichainAccountDetailsPage { await netoworksRow.click(); } - async getTruncatedAccountAddressFromNetworksPage(options: { - accountRowIndex: number; - }): Promise { - console.log( - 'Get truncated account address from account details > networks page', - ); - const { accountRowIndex } = options; - const accountRows = await this.driver.findElements( - '[data-testid="multichain-address-row-address"]', - ); - if (accountRowIndex < 0 || accountRowIndex >= accountRows.length) { - throw new Error('Invalid account row index'); - } - const accountRow = accountRows[accountRowIndex]; - const address = await accountRow.getText(); - return address; - } - /** * Click on the private key row */ diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index c3be8d5c3051..93c90313ff8e 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -426,13 +426,6 @@ export default function reduceApp( alertMessage: null, }; - case actionConstants.SET_ACCOUNT_DETAILS_ADDRESS: { - return { - ...appState, - accountDetailsAddress: action.payload, - }; - } - // qr scanner methods case actionConstants.QR_CODE_DETECTED: return { diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 08c181c0dda9..08145882f828 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -60,7 +60,6 @@ export const SHOW_SEND_TOKEN_PAGE = 'SHOW_SEND_TOKEN_PAGE'; export const SHOW_PRIVATE_KEY = 'SHOW_PRIVATE_KEY'; export const SET_ACCOUNT_LABEL = 'SET_ACCOUNT_LABEL'; export const CLEAR_ACCOUNT_DETAILS = 'CLEAR_ACCOUNT_DETAILS'; -export const SET_ACCOUNT_DETAILS_ADDRESS = 'SET_ACCOUNT_DETAILS_ADDRESS'; // tx conf screen export const COMPLETED_TX = 'COMPLETED_TX'; export const TRANSACTION_ERROR = 'TRANSACTION_ERROR'; From 1adc39319e36010c6347587681218bf503a09cd2 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 26 Nov 2025 14:40:39 +0100 Subject: [PATCH 11/11] fix: add back wallet details link but target state 2 page --- .../multichain-accounts-tree.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx b/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx index 409177000369..d7cedccef04f 100644 --- a/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx +++ b/ui/components/multichain-accounts/multichain-accounts-tree/multichain-accounts-tree.tsx @@ -1,5 +1,6 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { InternalAccount } from '@metamask/keyring-internal-api'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { Box, ButtonLink, ButtonLinkSize, Text } from '../../component-library'; import { AlignItems, @@ -16,6 +17,7 @@ import { import { ConsolidatedWallets } from '../../../selectors/multichain-accounts/account-tree.types'; import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { HiddenAccountList } from '../../multichain/account-list-menu/hidden-account-list'; +import { MULTICHAIN_WALLET_DETAILS_PAGE_ROUTE } from '../../../helpers/constants/routes'; import { matchesSearchPattern } from './utils'; export type MultichainAccountsTreeProps = { @@ -43,6 +45,18 @@ export const MultichainAccountsTree = ({ onClose, onAccountTreeItemClick, }: MultichainAccountsTreeProps) => { + const navigate = useNavigate(); + + const handleWalletDetailsClick = useCallback( + (walletId: string) => { + navigate( + `${MULTICHAIN_WALLET_DETAILS_PAGE_ROUTE}/${encodeURIComponent(walletId)}`, + ); + onClose(); + }, + [navigate, onClose], + ); + const accountsTree = useMemo(() => { // We keep a flag to check if there are any hidden accounts let hasHiddenAccounts: boolean = false; @@ -72,6 +86,7 @@ export const MultichainAccountsTree = ({ size={ButtonLinkSize.Sm} color={TextColor.primaryDefault} fontWeight={FontWeight.Medium} + onClick={() => handleWalletDetailsClick(walletId)} style={{ fontSize: '0.875rem', }} @@ -175,6 +190,7 @@ export const MultichainAccountsTree = ({ accountTreeItemProps, selectedAccount, onAccountTreeItemClick, + handleWalletDetailsClick, ]); return <>{accountsTree};