diff --git a/test/e2e/flask/multi-srp/import-srp.spec.ts b/test/e2e/flask/multi-srp/import-srp.spec.ts index ecf560c82b66..c9f737c56ed8 100644 --- a/test/e2e/flask/multi-srp/import-srp.spec.ts +++ b/test/e2e/flask/multi-srp/import-srp.spec.ts @@ -40,7 +40,7 @@ describe('Multi SRP - Import SRP', function (this: Suite) { }, async (driver: Driver) => { const accountListPage = new AccountListPage(driver); - await accountListPage.checkAccountBelongsToSrp('Account 2', 2); + await accountListPage.checkAccountBelongsToSrp('Account 1', 2); }, ); }); diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 8ed190d4760b..9cfcc24c12ba 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -404,7 +404,15 @@ class AccountListPage { const createMultichainAccountButtons = await this.driver.findElements( this.addMultichainAccountButton, ); - await createMultichainAccountButtons[options?.srpIndex ?? 0].click(); + const buttonIndex = options?.srpIndex ?? 0; + await createMultichainAccountButtons[buttonIndex].click(); + + // Wait for the account creation to complete by waiting for loading state to finish + // The button shows "Adding account..." during loading and "Add account" when done + await this.driver.assertElementNotPresent({ + css: this.addMultichainAccountButton, + text: 'Adding account...', + }); } /** @@ -1058,6 +1066,7 @@ class AccountListPage { if (srpIndex === 0) { throw new Error('SRP index must be > 0'); } + const srps = await this.driver.findElements('.select-srp__container'); const selectedSrp = srps[srpIndex - 1]; const showAccountsButton = await this.driver.waitForSelector( diff --git a/ui/components/multichain/multi-srp/srp-list/index.scss b/ui/components/multichain/multi-srp/srp-list/index.scss index 6838850bea9b..e90c51f247b0 100644 --- a/ui/components/multichain/multi-srp/srp-list/index.scss +++ b/ui/components/multichain/multi-srp/srp-list/index.scss @@ -28,6 +28,6 @@ &__account-name { overflow: hidden; white-space: nowrap; - max-width: 80px; + max-width: 120px; } } diff --git a/ui/components/multichain/multi-srp/srp-list/index.ts b/ui/components/multichain/multi-srp/srp-list/index.ts index 1675f9c1e48b..b27d5479e1d1 100644 --- a/ui/components/multichain/multi-srp/srp-list/index.ts +++ b/ui/components/multichain/multi-srp/srp-list/index.ts @@ -1 +1,2 @@ export { SrpList } from './srp-list'; +export { SrpListItem } from './srp-list-item'; diff --git a/ui/components/multichain/multi-srp/srp-list/srp-card.test.tsx b/ui/components/multichain/multi-srp/srp-list/srp-card.test.tsx new file mode 100644 index 000000000000..26a563b56ed4 --- /dev/null +++ b/ui/components/multichain/multi-srp/srp-list/srp-card.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { fireEvent } from '@testing-library/react'; + +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { AccountGroupId, AccountWalletId } from '@metamask/account-api'; + +import { renderWithProvider } from '../../../../../test/lib/render-helpers-navigate'; +import mockState from '../../../../../test/data/mock-state.json'; +import { FirstTimeFlowType } from '../../../../../shared/constants/onboarding'; +import { SrpCard } from './srp-card'; + +const mockWalletId = 'mock-wallet-id' as AccountWalletId; +const mockTotalFiatBalance = '$100.00'; + +const mocks = { + useSingleWalletAccountsBalanceCallback: jest + .fn() + .mockReturnValue((_: AccountGroupId) => mockTotalFiatBalance), + onActionComplete: jest.fn(), + useWalletInfoCallback: jest.fn().mockReturnValue({ + multichainAccounts: [ + { + id: 'mock-account-id-1' as AccountGroupId, + metadata: { + name: 'Mock Account 1', + }, + }, + ], + keyringId: '01JKAF3DSGM3AB87EM9N0K41AJ', + isSRPBackedUp: true, + }), +}; + +jest.mock('../../../../hooks/multichain-accounts/useWalletBalance', () => ({ + useSingleWalletAccountsBalanceCallback: (walletId: AccountWalletId) => + mocks.useSingleWalletAccountsBalanceCallback(walletId), +})); + +jest.mock('../../../../hooks/multichain-accounts/useWalletInfo', () => ({ + useWalletInfo: (walletId: AccountWalletId) => + mocks.useWalletInfoCallback(walletId), +})); + +const mockSecondHdKeyring = { + accounts: [], + type: KeyringTypes.hd, + metadata: { + id: '01JN31PKMJ3ANWYFJZM3Z8MYT4', + name: '', + }, +}; + +const render = (shouldTriggerBackup: boolean) => { + const store = configureMockStore([thunk])({ + ...mockState, + metamask: { + ...mockState.metamask, + keyrings: [...mockState.metamask.keyrings, mockSecondHdKeyring], + firstTimeFlowType: FirstTimeFlowType.create, + seedPhraseBackedUp: false, + }, + }); + + return renderWithProvider( + , + store, + ); +}; + +describe('SrpCard', () => { + it('renders the secret recovery phrases card', () => { + const { getByText } = render(false); + expect(getByText('Secret Recovery Phrase 1')).toBeInTheDocument(); + }); + + it('shows/hides accounts when clicking show/hide text', () => { + const { getByText } = render(false); + const showAccountsButton = getByText('Show 1 account'); + fireEvent.click(showAccountsButton); + expect(getByText('Hide 1 account')).toBeInTheDocument(); + }); + + it('calls onActionComplete when clicking a keyring', () => { + const { getByTestId } = render(true); + const firstKeyringId = mockState.metamask.keyrings[0].metadata.id; + + const keyring = getByTestId(`hd-keyring-${firstKeyringId}`); + fireEvent.click(keyring); + + expect(mocks.onActionComplete).toHaveBeenCalledWith(firstKeyringId, true); + }); +}); diff --git a/ui/components/multichain/multi-srp/srp-list/srp-card.tsx b/ui/components/multichain/multi-srp/srp-list/srp-card.tsx new file mode 100644 index 000000000000..b14792dff107 --- /dev/null +++ b/ui/components/multichain/multi-srp/srp-list/srp-card.tsx @@ -0,0 +1,174 @@ +import React, { useState, useContext, useCallback } from 'react'; +import type { AccountWalletId } from '@metamask/account-api'; + +import { MetaMetricsContext } from '../../../../contexts/metametrics'; +import { useWalletInfo } from '../../../../hooks/multichain-accounts/useWalletInfo'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { useSingleWalletAccountsBalanceCallback } from '../../../../hooks/multichain-accounts/useWalletBalance'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../../shared/constants/metametrics'; + +import Card from '../../../ui/card'; +import { + Box, + IconName, + Icon, + Text, + IconSize, +} from '../../../component-library'; +import { + JustifyContent, + Display, + TextColor, + FlexDirection, + AlignItems, + BlockSize, + TextVariant, + IconColor, +} from '../../../../helpers/constants/design-system'; +import { SrpListItem } from './srp-list-item'; + +/** + * Props for the SrpCard component. + */ +type SrpCardProps = { + index: number; + walletId: AccountWalletId; + shouldTriggerBackup: boolean; + onActionComplete: (id: string, triggerBackup?: boolean) => void; + isSettingsPage?: boolean; + hideShowAccounts?: boolean; +}; + +export const SrpCard = ({ + index, + walletId, + shouldTriggerBackup, + onActionComplete, + isSettingsPage = false, + hideShowAccounts = false, +}: SrpCardProps) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const { multichainAccounts, keyringId } = useWalletInfo(walletId); + const [showAccounts, setShowAccounts] = useState(false); + const walletAccountBalance = useSingleWalletAccountsBalanceCallback(walletId); + + const showHideText = useCallback( + (numberOfAccounts: number): string => { + if (numberOfAccounts > 1) { + return showAccounts + ? t('SrpListHideAccounts', [numberOfAccounts]) + : t('SrpListShowAccounts', [numberOfAccounts]); + } + return showAccounts + ? t('SrpListHideSingleAccount', [numberOfAccounts]) + : t('SrpListShowSingleAccount', [numberOfAccounts]); + }, + [showAccounts, t], + ); + + return ( + { + trackEvent({ + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.SecretRecoveryPhrasePickerClicked, + properties: { + // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 + // eslint-disable-next-line @typescript-eslint/naming-convention + button_type: 'srp_select', + }, + }); + keyringId && onActionComplete(keyringId, shouldTriggerBackup); + }} + className="select-srp__container" + marginBottom={3} + > + + + + {t('srpListName', [index + 1])} + + {!hideShowAccounts && ( + { + event.stopPropagation(); + trackEvent({ + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.SecretRecoveryPhrasePickerClicked, + properties: { + // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 + // eslint-disable-next-line @typescript-eslint/naming-convention + button_type: 'details', + }, + }); + setShowAccounts((prevState) => !prevState); + }} + > + {showHideText(multichainAccounts.length)} + + )} + + + {isSettingsPage && ( + + {shouldTriggerBackup + ? t('srpListStateNotBackedUp') + : t('srpListStateBackedUp')} + + )} + + + + {showAccounts && ( + + + {multichainAccounts.map((group) => { + return ( + + ); + })} + + )} + + ); +}; diff --git a/ui/components/multichain/multi-srp/srp-list/srp-list-item.test.tsx b/ui/components/multichain/multi-srp/srp-list/srp-list-item.test.tsx index 6488258270d8..52fe212fb6e9 100644 --- a/ui/components/multichain/multi-srp/srp-list/srp-list-item.test.tsx +++ b/ui/components/multichain/multi-srp/srp-list/srp-list-item.test.tsx @@ -1,65 +1,42 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { type AccountGroupId } from '@metamask/account-api'; + import mockState from '../../../../../test/data/mock-state.json'; -import { createMockInternalAccount } from '../../../../../test/jest/mocks'; -import { InternalAccountWithBalance } from '../../../../selectors'; import { renderWithProvider } from '../../../../../test/lib/render-helpers-navigate'; -import { shortenAddress } from '../../../../helpers/utils/util'; import { SrpListItem } from './srp-list-item'; -const mockTotalFiatBalance = '100'; -const mockAccount: InternalAccountWithBalance = { - ...createMockInternalAccount({ - name: 'Test Account', - address: '0xB1BAF6A2f4A808937bb97a2F12CCF08F1233e3D9', - }), - balance: mockTotalFiatBalance, -}; - -const mocks = { - useMultichainAccountTotalFiatBalance: jest.fn().mockReturnValue({ - totalFiatBalance: mockTotalFiatBalance, - }), -}; - -jest.mock('../../../../hooks/useMultichainAccountTotalFiatBalance', () => ({ - useMultichainAccountTotalFiatBalance: (account: InternalAccountWithBalance) => - mocks.useMultichainAccountTotalFiatBalance(account), -})); -jest.mock('../../../../helpers/utils/util', () => ({ - ...jest.requireActual('../../../../helpers/utils/util'), -})); +const mockAccountId = 'mock-account-id' as AccountGroupId; +const mockAccountName = 'Mock Account Name'; +const mockBalance = '$100.00'; const render = () => { const store = configureMockStore([thunk])(mockState); - return renderWithProvider(, store); + return renderWithProvider( + , + store, + ); }; describe('SrpListItem', () => { - beforeEach(() => { - // Reset mock implementations before each test - mocks.useMultichainAccountTotalFiatBalance.mockReturnValue({ - totalFiatBalance: mockTotalFiatBalance, - }); - }); - afterEach(() => { jest.clearAllMocks(); }); - it('renders account name and shortened address', () => { + it('renders account name', () => { const { getByText } = render(); - expect(getByText(mockAccount.metadata.name)).toBeInTheDocument(); - expect(getByText(shortenAddress(mockAccount.address))).toBeInTheDocument(); + expect(getByText(mockAccountName)).toBeInTheDocument(); }); it('calls useMultichainAccountTotalFiatBalance with correct account', () => { - render(); + const { getByText } = render(); - expect(mocks.useMultichainAccountTotalFiatBalance).toHaveBeenCalledWith( - mockAccount, - ); + expect(getByText(mockBalance)).toBeInTheDocument(); }); }); diff --git a/ui/components/multichain/multi-srp/srp-list/srp-list-item.tsx b/ui/components/multichain/multi-srp/srp-list/srp-list-item.tsx index 938e03135b93..a1f5a215aaa1 100644 --- a/ui/components/multichain/multi-srp/srp-list/srp-list-item.tsx +++ b/ui/components/multichain/multi-srp/srp-list/srp-list-item.tsx @@ -1,48 +1,36 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; -import { isEvmAccountType } from '@metamask/keyring-api'; -import { - AvatarAccount, - AvatarAccountSize, -} from '@metamask/design-system-react'; -import { InternalAccountWithBalance } from '../../../../selectors'; -import { useMultichainAccountTotalFiatBalance } from '../../../../hooks/useMultichainAccountTotalFiatBalance'; +import { type AccountGroupId } from '@metamask/account-api'; +import { AvatarAccountSize } from '@metamask/design-system-react'; +import { PreferredAvatar } from '../../../app/preferred-avatar'; import { Display, FlexDirection, AlignItems, JustifyContent, TextVariant, - TextColor, } from '../../../../helpers/constants/design-system'; -import { shortenAddress } from '../../../../helpers/utils/util'; -import UserPreferencedCurrencyDisplay from '../../../app/user-preferenced-currency-display'; +import { getIconSeedAddressByAccountGroupId } from '../../../../selectors/multichain-accounts/account-tree'; import { Text, Box } from '../../../component-library'; -// eslint-disable-next-line import/no-restricted-paths -import { normalizeSafeAddress } from '../../../../../app/scripts/lib/multichain/address'; -import { getMultichainAggregatedBalance } from '../../../../selectors/assets'; type SrpListItemProps = { - account: InternalAccountWithBalance; + accountId: AccountGroupId; + accountName: string; + balance: string; }; -export const SrpListItem = ({ account }: SrpListItemProps) => { - const { totalFiatBalance } = useMultichainAccountTotalFiatBalance(account); - const isEvmAccount = isEvmAccountType(account.type); - const multichainAggregatedBalance = useSelector((state) => - getMultichainAggregatedBalance(state, account), +export const SrpListItem = ({ + accountId, + accountName, + balance, +}: SrpListItemProps) => { + const seedAddress = useSelector((state) => + getIconSeedAddressByAccountGroupId(state, accountId), ); - const balance = useMemo(() => { - if (isEvmAccount) { - return totalFiatBalance; - } - return multichainAggregatedBalance.toString(); - }, [isEvmAccount, multichainAggregatedBalance, totalFiatBalance]); - return ( { flexDirection={FlexDirection.Row} alignItems={AlignItems.center} > - + - {account.metadata.name} - - - {shortenAddress(normalizeSafeAddress(account.address))} + {accountName} - - - + {balance} ); }; diff --git a/ui/components/multichain/multi-srp/srp-list/srp-list.test.tsx b/ui/components/multichain/multi-srp/srp-list/srp-list.test.tsx index 8bd26ab7abc9..3d8075dcb69f 100644 --- a/ui/components/multichain/multi-srp/srp-list/srp-list.test.tsx +++ b/ui/components/multichain/multi-srp/srp-list/srp-list.test.tsx @@ -3,26 +3,30 @@ import { fireEvent } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { KeyringTypes } from '@metamask/keyring-controller'; +import type { AccountGroupId, AccountWalletId } from '@metamask/account-api'; import { renderWithProvider } from '../../../../../test/lib/render-helpers-navigate'; import mockState from '../../../../../test/data/mock-state.json'; -import { InternalAccountWithBalance } from '../../../../selectors'; -import { shortenAddress } from '../../../../helpers/utils/util'; -// eslint-disable-next-line import/no-restricted-paths -import { normalizeSafeAddress } from '../../../../../app/scripts/lib/multichain/address'; import { FirstTimeFlowType } from '../../../../../shared/constants/onboarding'; import { SrpList } from './srp-list'; -const mockTotalFiatBalance = '100'; +const mockTotalFiatBalance = '$100.00'; + const mocks = { - useMultichainAccountTotalFiatBalance: jest.fn().mockReturnValue({ - totalFiatBalance: mockTotalFiatBalance, - }), + useSingleWalletAccountsBalanceCallback: jest + .fn() + .mockReturnValue((_: AccountGroupId) => mockTotalFiatBalance), onActionComplete: jest.fn(), + useWalletInfoCallback: jest.fn(), }; -jest.mock('../../../../hooks/useMultichainAccountTotalFiatBalance', () => ({ - useMultichainAccountTotalFiatBalance: (account: InternalAccountWithBalance) => - mocks.useMultichainAccountTotalFiatBalance(account), +jest.mock('../../../../hooks/multichain-accounts/useWalletBalance', () => ({ + useSingleWalletAccountsBalanceCallback: (walletId: AccountWalletId) => + mocks.useSingleWalletAccountsBalanceCallback(walletId), +})); + +jest.mock('../../../../hooks/multichain-accounts/useWalletInfo', () => ({ + useWalletInfo: (walletId: AccountWalletId) => + mocks.useWalletInfoCallback(walletId), })); const mockSecondHdKeyring = { @@ -34,12 +38,73 @@ const mockSecondHdKeyring = { }, }; +// Second wallet entry for accountTree to match the second keyring +const mockSecondWallet = { + id: 'entropy:01JN31PKMJ3ANWYFJZM3Z8MYT4' as AccountWalletId, + type: 'entropy', + groups: { + 'entropy:01JN31PKMJ3ANWYFJZM3Z8MYT4/0': { + id: 'entropy:01JN31PKMJ3ANWYFJZM3Z8MYT4/0' as AccountGroupId, + type: 'multichain-account', + accounts: ['mock-account-id-2'], + metadata: { + name: 'Account 2', + entropy: { groupIndex: 0 }, + hidden: false, + pinned: false, + }, + }, + }, + metadata: { + name: 'Wallet 2', + entropy: { id: '01JN31PKMJ3ANWYFJZM3Z8MYT4' }, + }, +}; + const render = () => { + // Set up useWalletInfo mock to return correct data for each wallet + mocks.useWalletInfoCallback.mockImplementation( + (walletId: AccountWalletId) => { + if (walletId === 'entropy:01JKAF3DSGM3AB87EM9N0K41AJ') { + return { + multichainAccounts: [ + { + id: 'entropy:01JKAF3DSGM3AB87EM9N0K41AJ/0' as AccountGroupId, + metadata: { name: 'Account 1' }, + }, + ], + keyringId: '01JKAF3DSGM3AB87EM9N0K41AJ', + isSRPBackedUp: false, + }; + } + if (walletId === 'entropy:01JN31PKMJ3ANWYFJZM3Z8MYT4') { + return { + multichainAccounts: [ + { + id: 'entropy:01JN31PKMJ3ANWYFJZM3Z8MYT4/0' as AccountGroupId, + metadata: { name: 'Account 2' }, + }, + ], + keyringId: '01JN31PKMJ3ANWYFJZM3Z8MYT4', + isSRPBackedUp: true, + }; + } + return { multichainAccounts: [], keyringId: undefined }; + }, + ); + const store = configureMockStore([thunk])({ ...mockState, metamask: { ...mockState.metamask, keyrings: [...mockState.metamask.keyrings, mockSecondHdKeyring], + accountTree: { + ...mockState.metamask.accountTree, + wallets: { + ...mockState.metamask.accountTree.wallets, + 'entropy:01JN31PKMJ3ANWYFJZM3Z8MYT4': mockSecondWallet, + }, + }, firstTimeFlowType: FirstTimeFlowType.create, seedPhraseBackedUp: false, }, @@ -58,13 +123,6 @@ describe('SrpList', () => { expect(getByText('Secret Recovery Phrase 2')).toBeInTheDocument(); }); - it('shows/hides accounts when clicking show/hide text', () => { - const { getByText } = render(); - const showAccountsButton = getByText('Show 2 accounts'); - fireEvent.click(showAccountsButton); - expect(getByText('Hide 2 accounts')).toBeInTheDocument(); - }); - it('calls onActionComplete when clicking a keyring', () => { const { getByTestId } = render(); const firstKeyringId = mockState.metamask.keyrings[0].metadata.id; @@ -74,28 +132,4 @@ describe('SrpList', () => { expect(mocks.onActionComplete).toHaveBeenCalledWith(firstKeyringId, true); }); - - it('displays the correct accounts for a keyring and ensures no duplicates', () => { - const { getByText, getAllByText } = render(); - const firstKeyringAccounts = mockState.metamask.keyrings[0].accounts; - const account1Address = firstKeyringAccounts[0]; - const account2Address = firstKeyringAccounts[1]; - - const showAccountsButton = getByText('Show 2 accounts'); - fireEvent.click(showAccountsButton); - - const shortenedAccount1 = shortenAddress( - normalizeSafeAddress(account1Address), - ); - const shortenedAccount2 = shortenAddress( - normalizeSafeAddress(account2Address), - ); - - expect(getByText(shortenedAccount1)).toBeInTheDocument(); - expect(getByText(shortenedAccount2)).toBeInTheDocument(); - - // Ensure no duplicates by checking the count of each shortened address. - expect(getAllByText(shortenedAccount1).length).toBe(1); - expect(getAllByText(shortenedAccount2).length).toBe(1); - }); }); diff --git a/ui/components/multichain/multi-srp/srp-list/srp-list.tsx b/ui/components/multichain/multi-srp/srp-list/srp-list.tsx index a109f7b6bf18..8b02f6b53b92 100644 --- a/ui/components/multichain/multi-srp/srp-list/srp-list.tsx +++ b/ui/components/multichain/multi-srp/srp-list/srp-list.tsx @@ -1,35 +1,12 @@ -import React, { useContext, useMemo, useState } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; -import Card from '../../../ui/card'; -import { - Box, - IconName, - Icon, - Text, - IconSize, -} from '../../../component-library'; -import { - JustifyContent, - Display, - TextColor, - FlexDirection, - AlignItems, - BlockSize, - TextVariant, - IconColor, -} from '../../../../helpers/constants/design-system'; -import { getMetaMaskAccounts } from '../../../../selectors/selectors'; -import { InternalAccountWithBalance } from '../../../../selectors/selectors.types'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../../../shared/constants/metametrics'; -import { MetaMetricsContext } from '../../../../contexts/metametrics'; -import { useHdKeyringsWithSnapAccounts } from '../../../../hooks/multi-srp/useHdKeyringsWithSnapAccounts'; +import { AccountWalletType } from '@metamask/account-api'; + +import { getWalletIdsByType } from '../../../../selectors/multichain-accounts/account-tree'; import { getIsPrimarySeedPhraseBackedUp } from '../../../../ducks/metamask/metamask'; -import { SrpListItem } from './srp-list-item'; +import { Box } from '../../../component-library'; +import { SrpCard } from './srp-card'; export const SrpList = ({ onActionComplete, @@ -40,38 +17,14 @@ export const SrpList = ({ isSettingsPage?: boolean; hideShowAccounts?: boolean; }) => { - const t = useI18nContext(); - const trackEvent = useContext(MetaMetricsContext); - const hdKeyringsWithSnapAccounts = useHdKeyringsWithSnapAccounts(); - const isPrimarySeedPhraseBackedUp = useSelector( getIsPrimarySeedPhraseBackedUp, ); - // This selector will return accounts with nonEVM balances as well. - const accountsWithBalances: Record = - useSelector(getMetaMaskAccounts); - - const showAccountsInitState = useMemo( - () => new Array(hdKeyringsWithSnapAccounts.length).fill(hideShowAccounts), - [hdKeyringsWithSnapAccounts, hideShowAccounts], - ); - - const [showAccounts, setShowAccounts] = useState( - showAccountsInitState, + const walletIdsFromStore = useSelector((state) => + getWalletIdsByType(state, AccountWalletType.Entropy), ); - const showHideText = (index: number, numberOfAccounts: number): string => { - if (numberOfAccounts > 1) { - return showAccounts[index] - ? t('SrpListHideAccounts', [numberOfAccounts]) - : t('SrpListShowAccounts', [numberOfAccounts]); - } - return showAccounts[index] - ? t('SrpListHideSingleAccount', [numberOfAccounts]) - : t('SrpListShowSingleAccount', [numberOfAccounts]); - }; - return ( - {hdKeyringsWithSnapAccounts.map((keyring, index) => { + {walletIdsFromStore.map((walletId, index) => { // We only consider the first(primary) keyring for the backup reminder. const shouldTriggerBackup = !isPrimarySeedPhraseBackedUp && index === 0; return ( - { - trackEvent({ - category: MetaMetricsEventCategory.Accounts, - event: MetaMetricsEventName.SecretRecoveryPhrasePickerClicked, - properties: { - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - button_type: 'srp_select', - }, - }); - onActionComplete(keyring.metadata.id, shouldTriggerBackup); - }} - className="select-srp__container" - marginBottom={3} - > - - - - {t('srpListName', [index + 1])} - - {!hideShowAccounts && ( - { - event.stopPropagation(); - trackEvent({ - category: MetaMetricsEventCategory.Accounts, - event: - MetaMetricsEventName.SecretRecoveryPhrasePickerClicked, - properties: { - // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 - // eslint-disable-next-line @typescript-eslint/naming-convention - button_type: 'details', - }, - }); - setShowAccounts((prevState) => { - const accountListLength = - hdKeyringsWithSnapAccounts.length; - let newState = prevState; - if (accountListLength > prevState.length) { - // Extend the state with `false` for new accounts - newState = [ - ...prevState, - ...Array(accountListLength - prevState.length).fill( - false, - ), - ]; - } - return newState.map((value, i) => - i === index ? !value : value, - ); - }); - }} - > - {showHideText(index, keyring.accounts.length)} - - )} - - - {isSettingsPage && ( - - {shouldTriggerBackup - ? t('srpListStateNotBackedUp') - : t('srpListStateBackedUp')} - - )} - - - - {showAccounts[index] && ( - - - {keyring.accounts.map((address: string) => { - const account = accountsWithBalances[address]; - return ( - - ); - })} - - )} - + ); })} diff --git a/ui/selectors/multichain-accounts/account-tree.ts b/ui/selectors/multichain-accounts/account-tree.ts index 236e99693b79..073415f65b4a 100644 --- a/ui/selectors/multichain-accounts/account-tree.ts +++ b/ui/selectors/multichain-accounts/account-tree.ts @@ -6,7 +6,10 @@ import { import { isEvmAccountType, EthAccountType } from '@metamask/keyring-api'; import { AccountId } from '@metamask/accounts-controller'; import { createSelector } from 'reselect'; -import { AccountGroupObject } from '@metamask/account-tree-controller'; +import { + type AccountWalletObject, + type AccountGroupObject, +} from '@metamask/account-tree-controller'; import { InternalAccount } from '@metamask/keyring-internal-api'; import { type Hex, @@ -906,3 +909,35 @@ export const getIconSeedAddressesByAccountGroups = ( return seedAddresses; }; + +/** + * Retrieve wallet IDs from account tree state by type. + * In case no type is provided it returns all wallet IDs. + * + * @param state - Redux state. + * @param state.metamask - MetaMask state object. + * @param state.metamask.accountTree - Account tree state object. + * @param walletId - The ID of the wallet to retrieve. + * @returns Wallet object from account tree state. + */ +export const getWalletIdsByType = createSelector( + (state: MultichainAccountsState) => state.metamask?.accountTree?.wallets, + (_, walletType?: AccountWalletType) => walletType, + ( + wallets: Record, + walletType?: AccountWalletType, + ): AccountWalletId[] => { + if (!wallets) { + return []; + } + + if (!walletType) { + // return all wallet IDs if no type is specified + return Object.keys(wallets) as AccountWalletId[]; + } + + return Object.entries(wallets) + .filter(([, wallet]) => wallet.type === walletType) + .map(([walletId]) => walletId) as AccountWalletId[]; + }, +);