Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/components/multichain/multi-srp/srp-card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SrpCard } from './srp-card';
99 changes: 99 additions & 0 deletions ui/components/multichain/multi-srp/srp-card/srp-card.test.tsx
Original file line number Diff line number Diff line change
@@ -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: [
{
accountId: '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(
<SrpCard
index={0}
walletId={mockWalletId}
shouldTriggerBackup={shouldTriggerBackup}
onActionComplete={mocks.onActionComplete}
/>,
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);
});
});
174 changes: 174 additions & 0 deletions ui/components/multichain/multi-srp/srp-card/srp-card.tsx
Original file line number Diff line number Diff line change
@@ -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';

/**
* 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<boolean>(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 (
<Card
key={`srp-${keyringId}`}
data-testid={`hd-keyring-${keyringId}`}
onClick={() => {
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}
>
<Box
display={Display.Flex}
flexDirection={FlexDirection.Row}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
>
<Box>
<Text variant={TextVariant.bodyMdMedium}>
{t('srpListName', [index + 1])}
</Text>
{!hideShowAccounts && (
<Text
variant={TextVariant.bodySm}
color={TextColor.primaryDefault}
className="srp-list__show-accounts"
data-testid={`srp-list-show-accounts-${index}`}
onClick={(event: React.MouseEvent) => {
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)}
</Text>
)}
</Box>
<Box display={Display.Flex} alignItems={AlignItems.center} gap={1}>
{isSettingsPage && (
<Text
variant={TextVariant.bodyMdMedium}
color={
shouldTriggerBackup
? TextColor.errorDefault
: TextColor.textAlternative
}
>
{shouldTriggerBackup
? t('srpListStateNotBackedUp')
: t('srpListStateBackedUp')}
</Text>
)}
<Icon
name={IconName.ArrowRight}
size={IconSize.Sm}
color={
shouldTriggerBackup && isSettingsPage
? IconColor.errorDefault
: IconColor.iconAlternative
}
/>
</Box>
</Box>
{showAccounts && (
<Box>
<Box
width={BlockSize.Full}
className="srp-list__divider"
marginTop={2}
marginBottom={2}
/>
{multichainAccounts.map((group) => {
return (
<SrpListItem
key={`account-${group.id}`}
accountId={group.id}
accountName={group.metadata.name}
balance={walletAccountBalance(group.id) ?? ''}
/>
);
})}
</Box>
)}
</Card>
);
};
2 changes: 1 addition & 1 deletion ui/components/multichain/multi-srp/srp-list/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
&__account-name {
overflow: hidden;
white-space: nowrap;
max-width: 80px;
max-width: 120px;
}
}
1 change: 1 addition & 0 deletions ui/components/multichain/multi-srp/srp-list/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SrpList } from './srp-list';
export { SrpListItem } from './srp-list-item';
Original file line number Diff line number Diff line change
@@ -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(<SrpListItem account={mockAccount} />, store);
return renderWithProvider(
<SrpListItem
accountId={mockAccountId}
accountName={mockAccountName}
balance={mockBalance}
/>,
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();
});
});
Loading
Loading