Skip to content

Commit 469e570

Browse files
authored
feat: Add SRP backup to Multichain Account Details (#35518)
## **Description** This PR adds SRP reveal/backup item to the Multichain Account List page. Notes: - Reveal SRP process and JSX template is extracted into a new component to reduce code duplication. - Some minor refactoring is done to Wallet Details and Account Details pages to improve maintainability. - Some minor UI adjustments are done in order to match the design specification from [Figma](https://www.figma.com/design/G88ChchQ8FINCBI9BBkrAG/BIP-44-Extension?node-id=0-1&p=f&m=dev). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/35518?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Added SRP backup process to Multichain Account Details ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MUL-701 ## **Manual testing steps** 1. Build extension with multichain accounts state 2 feature flag enabled. 2. Open extension > account list > account details. 3. Check that account details contains SRP backup row. 4. Use SRP backup process by clicking on the SRP backup row and test. 5. Check the same process on wallet details. 6. Check the same process for new fresh wallet (single wallet) for which backup of SRP is skipped on creation. Make sure that backup text is red and the process links to the SRP backup page instead to a modal. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** SRP reveal/backup was not available on the account details page before, so nothing important to show here. Please have a look at the "After" section and confirm that everything is as expected and by design. ### **After** https://github.com/user-attachments/assets/d4922256-7746-402f-bfea-2e6e6f14d7fd https://github.com/user-attachments/assets/1e9eb1b4-0f6a-4437-bb49-a1767962df82 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent 1df169b commit 469e570

File tree

12 files changed

+337
-99
lines changed

12 files changed

+337
-99
lines changed

ui/components/multichain-accounts/account-details-row/account-details-row.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const AccountDetailsRow = ({
3434

3535
return (
3636
<Box
37-
backgroundColor={BackgroundColor.backgroundAlternative}
37+
backgroundColor={BackgroundColor.backgroundMuted}
3838
display={Display.Flex}
3939
justifyContent={JustifyContent.spaceBetween}
4040
style={style}

ui/components/multichain-accounts/account-details-row/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525

2626
&__value {
2727
max-width: 150px;
28+
font-weight: 600;
2829
}
2930
}

ui/components/multichain-accounts/multichain-accounts.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
@import "multichain-account-menu-items";
44
@import "account-details-row";
55
@import "add-multichain-account";
6+
@import "multichain-srp-backup";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.multichain-srp-backup {
2+
p {
3+
font-weight: 600;
4+
}
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MultichainSrpBackup } from './multichain-srp-backup';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import { StoryObj, Meta } from '@storybook/react';
3+
import { MemoryRouter } from 'react-router-dom';
4+
import { Provider } from 'react-redux';
5+
import configureStore from '../../../store/store';
6+
import mockState from '../../../../test/data/mock-state.json';
7+
import { MultichainSrpBackup } from './multichain-srp-backup';
8+
9+
const meta: Meta<typeof MultichainSrpBackup> = {
10+
title: 'Components/MultichainAccounts/MultichainSrpBackup',
11+
component: MultichainSrpBackup,
12+
parameters: {
13+
docs: {
14+
description: {
15+
component:
16+
'A component that displays a Secret Recovery Phrase backup button.',
17+
},
18+
},
19+
},
20+
decorators: [
21+
(Story) => {
22+
const store = configureStore({
23+
metamask: {
24+
...mockState.metamask,
25+
},
26+
});
27+
28+
return (
29+
<Provider store={store}>
30+
<MemoryRouter>
31+
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
32+
<Story />
33+
</div>
34+
</MemoryRouter>
35+
</Provider>
36+
);
37+
},
38+
],
39+
argTypes: {
40+
shouldShowBackupReminder: {
41+
control: 'boolean',
42+
description: 'When true, displays a backup reminder with error styling',
43+
defaultValue: false,
44+
},
45+
className: {
46+
control: 'text',
47+
description: 'Additional CSS class names',
48+
},
49+
keyringId: {
50+
control: 'text',
51+
description: 'ID of the keyring for SRP quiz modal',
52+
},
53+
},
54+
};
55+
56+
export default meta;
57+
type Story = StoryObj<typeof MultichainSrpBackup>;
58+
59+
export const Default: Story = {
60+
args: {
61+
shouldShowBackupReminder: false,
62+
keyringId: 'test-keyring-id',
63+
},
64+
};
65+
66+
export const WithBackupReminder: Story = {
67+
args: {
68+
shouldShowBackupReminder: true,
69+
keyringId: 'test-keyring-id',
70+
},
71+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from 'react';
2+
import { screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { renderWithProvider } from '../../../../test/lib/render-helpers';
4+
import mockState from '../../../../test/data/mock-state.json';
5+
import configureStore from '../../../store/store';
6+
import { ONBOARDING_REVIEW_SRP_ROUTE } from '../../../helpers/constants/routes';
7+
import { MultichainSrpBackup } from './multichain-srp-backup';
8+
9+
const mockHistoryPush = jest.fn();
10+
jest.mock('react-router-dom', () => ({
11+
...jest.requireActual('react-router-dom'),
12+
useHistory: () => ({
13+
push: mockHistoryPush,
14+
}),
15+
}));
16+
17+
const srpBackupRowTestId = 'multichain-srp-backup';
18+
const srpQuizHeaderTestId = 'srp-quiz-header';
19+
20+
describe('MultichainSrpBackup', () => {
21+
const renderComponent = (props = {}) => {
22+
const store = configureStore({
23+
metamask: {
24+
...mockState.metamask,
25+
},
26+
});
27+
28+
return renderWithProvider(<MultichainSrpBackup {...props} />, store);
29+
};
30+
31+
beforeEach(() => {
32+
jest.clearAllMocks();
33+
});
34+
35+
it('renders with default props', () => {
36+
renderComponent();
37+
38+
expect(screen.getByText('Secret Recovery Phrase')).toBeInTheDocument();
39+
expect(screen.getByText('Back up')).toBeInTheDocument();
40+
41+
const buttonElement = screen.getByTestId(srpBackupRowTestId);
42+
expect(buttonElement).toHaveClass('multichain-srp-backup');
43+
});
44+
45+
it('applies custom className when provided', () => {
46+
renderComponent({ className: 'custom-class' });
47+
48+
const buttonElement = screen.getByTestId(srpBackupRowTestId);
49+
expect(buttonElement).toHaveClass('multichain-srp-backup');
50+
expect(buttonElement).toHaveClass('custom-class');
51+
});
52+
53+
it('displays "Back up" text when shouldShowBackupReminder is false', () => {
54+
renderComponent({ shouldShowBackupReminder: false });
55+
56+
expect(screen.getByText('Back up')).toBeInTheDocument();
57+
expect(screen.queryByText('Backup')).not.toBeInTheDocument();
58+
});
59+
60+
it('navigates to SRP review route when shouldShowBackupReminder is true', () => {
61+
renderComponent({ shouldShowBackupReminder: true });
62+
63+
fireEvent.click(screen.getByTestId(srpBackupRowTestId));
64+
65+
expect(mockHistoryPush).toHaveBeenCalledWith(
66+
`${ONBOARDING_REVIEW_SRP_ROUTE}/?isFromReminder=true`,
67+
);
68+
});
69+
70+
it('opens SRP quiz modal when shouldShowBackupReminder is false', async () => {
71+
renderComponent({
72+
shouldShowBackupReminder: false,
73+
keyringId: 'test-keyring-id',
74+
});
75+
76+
fireEvent.click(screen.getByTestId(srpBackupRowTestId));
77+
78+
await waitFor(() => {
79+
expect(screen.getByText('Security quiz')).toBeInTheDocument();
80+
});
81+
82+
expect(mockHistoryPush).not.toHaveBeenCalled();
83+
});
84+
85+
it('closes SRP quiz modal when close button is clicked', async () => {
86+
renderComponent({
87+
shouldShowBackupReminder: false,
88+
keyringId: 'test-keyring-id',
89+
});
90+
91+
fireEvent.click(screen.getByTestId(srpBackupRowTestId));
92+
await waitFor(() => {
93+
expect(screen.getByText('Security quiz')).toBeInTheDocument();
94+
});
95+
96+
const closeButton = screen
97+
.getByTestId(srpQuizHeaderTestId)
98+
.querySelector('button');
99+
100+
if (closeButton) {
101+
fireEvent.click(closeButton);
102+
} else {
103+
throw new Error('Close button not found in the modal header');
104+
}
105+
106+
await waitFor(() => {
107+
expect(screen.queryByText('Security quiz')).not.toBeInTheDocument();
108+
});
109+
});
110+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { useCallback, useState } from 'react';
2+
import { useHistory } from 'react-router-dom';
3+
import classnames from 'classnames';
4+
import { IconSize } from '@metamask/design-system-react';
5+
import { Box, Icon, IconName, Text } from '../../component-library';
6+
import {
7+
AlignItems,
8+
BackgroundColor,
9+
BlockSize,
10+
Display,
11+
IconColor,
12+
JustifyContent,
13+
TextAlign,
14+
TextColor,
15+
TextVariant,
16+
} from '../../../helpers/constants/design-system';
17+
import { useI18nContext } from '../../../hooks/useI18nContext';
18+
import { ONBOARDING_REVIEW_SRP_ROUTE } from '../../../helpers/constants/routes';
19+
import SRPQuiz from '../../app/srp-quiz-modal';
20+
21+
export type MultichainSrpBackupProps = {
22+
shouldShowBackupReminder?: boolean;
23+
className?: string | Record<string, boolean>;
24+
keyringId?: string;
25+
};
26+
27+
export const MultichainSrpBackup: React.FC<MultichainSrpBackupProps> = ({
28+
shouldShowBackupReminder = false,
29+
className = '',
30+
keyringId,
31+
}) => {
32+
const t = useI18nContext();
33+
const history = useHistory();
34+
const [srpQuizModalVisible, setSrpQuizModalVisible] = useState(false);
35+
36+
const handleSrpBackupClick = useCallback(() => {
37+
if (shouldShowBackupReminder) {
38+
const backUpSRPRoute = `${ONBOARDING_REVIEW_SRP_ROUTE}/?isFromReminder=true`;
39+
history.push(backUpSRPRoute);
40+
} else {
41+
setSrpQuizModalVisible(true);
42+
}
43+
}, [shouldShowBackupReminder, history, setSrpQuizModalVisible]);
44+
45+
const handleQuizModalClose = useCallback(() => {
46+
setSrpQuizModalVisible(false);
47+
}, [setSrpQuizModalVisible]);
48+
49+
const finalClassName = classnames('multichain-srp-backup', className);
50+
51+
return (
52+
<>
53+
<Box
54+
className={finalClassName}
55+
padding={4}
56+
width={BlockSize.Full}
57+
textAlign={TextAlign.Left}
58+
display={Display.Flex}
59+
justifyContent={JustifyContent.spaceBetween}
60+
alignItems={AlignItems.center}
61+
backgroundColor={BackgroundColor.backgroundMuted}
62+
onClick={handleSrpBackupClick}
63+
data-testid="multichain-srp-backup"
64+
>
65+
<Box>
66+
<Text
67+
variant={TextVariant.bodyMdMedium}
68+
color={TextColor.textDefault}
69+
>
70+
{t('secretRecoveryPhrase')}
71+
</Text>
72+
</Box>
73+
<Box display={Display.Flex} alignItems={AlignItems.center} gap={2}>
74+
<Text
75+
variant={TextVariant.bodyMdMedium}
76+
color={TextColor.textAlternative}
77+
>
78+
{t('accountDetailsSrpBackUpMessage')}
79+
</Text>
80+
<Icon
81+
name={IconName.ArrowRight}
82+
size={IconSize.Sm}
83+
color={IconColor.iconAlternative}
84+
/>
85+
</Box>
86+
</Box>
87+
{srpQuizModalVisible && keyringId && (
88+
<SRPQuiz
89+
keyringId={keyringId}
90+
isOpen={srpQuizModalVisible}
91+
onClose={handleQuizModalClose}
92+
closeAfterCompleting
93+
/>
94+
)}
95+
</>
96+
);
97+
};

ui/pages/multichain-accounts/multichain-account-details-page/index.scss

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.multichain-account-details {
1+
.multichain-account-details-page {
22
max-width: 600px;
33

44
&__section {
@@ -18,6 +18,13 @@
1818
}
1919
}
2020

21+
&__srp-button {
22+
cursor: pointer;
23+
border: none;
24+
padding-left: 12px;
25+
padding-right: 20px;
26+
}
27+
2128
&__remove-button {
2229
font-weight: 600;
2330
}

ui/pages/multichain-accounts/multichain-account-details-page/multichain-account-details-page.test.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ const accountDetailsRowNetworksTestId = 'account-details-row-networks';
1212
const accountDetailsRowPrivateKeysTestId = 'account-details-row-private-keys';
1313
const accountDetailsRowSmartAccountTestId = 'account-details-row-smart-account';
1414
const accountDetailsRowWalletTestId = 'account-details-row-wallet';
15-
const accountDetailsRowSecretRecoveryPhraseTestId =
16-
'account-details-row-secret-recovery-phrase';
15+
const accountDetailsRowSecretRecoveryPhraseTestId = 'multichain-srp-backup';
1716

1817
const mockHistoryPush = jest.fn();
1918
const mockHistoryGoBack = jest.fn();

0 commit comments

Comments
 (0)