Skip to content

Commit 1df169b

Browse files
feat: Add initial multichain add wallet page for private key import (#35543)
## **Description** This PR introduces a new dedicated add wallet page (`/add-wallet-page`) for importing private keys and JSON wallets. This addresses the issue where the previous import wallet functionality was not conditionally rendered and had no proper route structure. **Key Changes:** - Creates a new `AddWalletPage` component that wraps the existing `ImportAccount` component - Adds proper routing configuration with `ADD_WALLET_PAGE_ROUTE = '/add-wallet-page'` - Provides a clean navigation experience with back button functionality - Includes comprehensive test coverage for the new page **Future Scope:** This page will eventually handle both SRP and private key account imports, but for now it focuses on private key import functionality only. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/35543?quickstart=1) ## **Changelog** CHANGELOG entry: Added dedicated add wallet page for private key import with proper routing ## **Related issues** Related to: #35536 ## **Manual testing steps** 1. Run storybook `yarn storybook` or view [storybook PR build](https://diuv6g5fj9pvx.cloudfront.net/metamask-extension/17388655285/storybook-build/index.html?path=/story/pages-multichainaccounts-addwalletpage--default) 2. Navigate to the Add wallet page 3. Ensure UI renders as expected ## **Screenshots/Recordings** ### **Before** - No add wallet page existed, only the import account modal ### **After** - New Add wallet page in storybook https://github.com/user-attachments/assets/244bbce6-e6fc-48fa-962f-e7d017b6bfd6 ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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 0d7e5ce commit 1df169b

File tree

6 files changed

+177
-0
lines changed

6 files changed

+177
-0
lines changed

ui/helpers/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token';
4444
export const ACCOUNT_LIST_PAGE_ROUTE = '/account-list';
4545
export const MULTICHAIN_ACCOUNT_ADDRESS_LIST_PAGE_ROUTE =
4646
'/multichain-account-address-list';
47+
export const ADD_WALLET_PAGE_ROUTE = '/add-wallet-page';
4748
export const MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE =
4849
'/multichain-account-details';
4950
export const MULTICHAIN_WALLET_DETAILS_PAGE_ROUTE =
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { StoryObj, Meta } from '@storybook/react';
2+
import { AddWalletPage } from './add-wallet-page';
3+
4+
const meta: Meta<typeof AddWalletPage> = {
5+
title: 'Pages/MultichainAccounts/AddWalletPage',
6+
component: AddWalletPage,
7+
};
8+
9+
export default meta;
10+
type Story = StoryObj<typeof AddWalletPage>;
11+
12+
export const Default: Story = {};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import { screen, fireEvent } from '@testing-library/react';
3+
import { renderWithProvider } from '../../../../test/lib/render-helpers';
4+
5+
import configureStore from '../../../store/store';
6+
import mockState from '../../../../test/data/mock-state.json';
7+
8+
import { AddWalletPage } from './add-wallet-page';
9+
10+
const mockHistoryGoBack = jest.fn();
11+
12+
jest.mock('react-router-dom', () => ({
13+
...jest.requireActual('react-router-dom'),
14+
useHistory: () => ({
15+
goBack: mockHistoryGoBack,
16+
}),
17+
}));
18+
19+
// Mock the ImportAccount component to test onActionComplete function is passed
20+
jest.mock(
21+
'../../../components/multichain/import-account/import-account',
22+
() => ({
23+
ImportAccount: ({
24+
onActionComplete,
25+
}: {
26+
onActionComplete: (success: boolean) => void;
27+
}) => (
28+
<div>
29+
<button onClick={() => onActionComplete(true)}>
30+
Mock Import Success
31+
</button>
32+
<button onClick={() => onActionComplete(false)}>
33+
Mock Import Failure
34+
</button>
35+
</div>
36+
),
37+
}),
38+
);
39+
40+
const backButtonTestId = 'add-wallet-page-back-button';
41+
42+
const renderComponent = () => {
43+
const store = configureStore({
44+
metamask: {
45+
...mockState.metamask,
46+
},
47+
});
48+
return renderWithProvider(<AddWalletPage />, store);
49+
};
50+
51+
describe('AddWalletPage', () => {
52+
beforeEach(() => {
53+
jest.clearAllMocks();
54+
});
55+
56+
it('renders the page with correct title and components', () => {
57+
renderComponent();
58+
59+
expect(screen.getByText('Add wallet')).toBeInTheDocument();
60+
expect(screen.getByText('Private key')).toBeInTheDocument();
61+
expect(screen.getByTestId(backButtonTestId)).toBeInTheDocument();
62+
});
63+
64+
it('calls history.goBack when back button is clicked', () => {
65+
renderComponent();
66+
67+
const backButton = screen.getByTestId(backButtonTestId);
68+
fireEvent.click(backButton);
69+
70+
expect(mockHistoryGoBack).toHaveBeenCalledTimes(1);
71+
});
72+
73+
it('handles successful import completion', () => {
74+
renderComponent();
75+
76+
const successButton = screen.getByRole('button', {
77+
name: 'Mock Import Success',
78+
});
79+
fireEvent.click(successButton);
80+
81+
expect(mockHistoryGoBack).toHaveBeenCalledTimes(1);
82+
});
83+
84+
it('does not navigate on failed import', () => {
85+
renderComponent();
86+
87+
const failureButton = screen.getByRole('button', {
88+
name: 'Mock Import Failure',
89+
});
90+
fireEvent.click(failureButton);
91+
92+
expect(mockHistoryGoBack).not.toHaveBeenCalled();
93+
});
94+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { useCallback } from 'react';
2+
3+
import { useHistory } from 'react-router-dom';
4+
import {
5+
ButtonIcon,
6+
ButtonIconSize,
7+
IconName,
8+
Text,
9+
TextVariant,
10+
} from '@metamask/design-system-react';
11+
import {
12+
Content,
13+
Header,
14+
Page,
15+
} from '../../../components/multichain/pages/page';
16+
import { TextVariant as LegacyTextVariant } from '../../../helpers/constants/design-system';
17+
import { useI18nContext } from '../../../hooks/useI18nContext';
18+
import { ImportAccount } from '../../../components/multichain/import-account/import-account';
19+
20+
/**
21+
*
22+
* TODO: This page will eventually handle both SRP and Private Key account imports
23+
* For now, it only handles Private Key account imports
24+
*/
25+
export const AddWalletPage = () => {
26+
const t = useI18nContext();
27+
const history = useHistory();
28+
29+
const onActionComplete = useCallback(
30+
async (confirmed: boolean) => {
31+
if (confirmed) {
32+
history.goBack();
33+
}
34+
},
35+
[history],
36+
);
37+
38+
return (
39+
<Page className="max-w-[600px]">
40+
<Header
41+
textProps={{
42+
variant: LegacyTextVariant.headingSm,
43+
}}
44+
startAccessory={
45+
<ButtonIcon
46+
size={ButtonIconSize.Md}
47+
ariaLabel={t('back')}
48+
iconName={IconName.ArrowLeft}
49+
onClick={() => history.goBack()}
50+
data-testid="add-wallet-page-back-button"
51+
/>
52+
}
53+
>
54+
{t('addWallet')}
55+
</Header>
56+
<Content>
57+
<Text variant={TextVariant.HeadingSm}>{t('privateKey')}</Text>
58+
<ImportAccount onActionComplete={onActionComplete} />
59+
</Content>
60+
</Page>
61+
);
62+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { AddWalletPage } from './add-wallet-page';

ui/pages/routes/routes.component.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
ACCOUNT_DETAILS_QR_CODE_ROUTE,
6262
ACCOUNT_LIST_PAGE_ROUTE,
6363
MULTICHAIN_ACCOUNT_ADDRESS_LIST_PAGE_ROUTE,
64+
ADD_WALLET_PAGE_ROUTE,
6465
MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE,
6566
MULTICHAIN_WALLET_DETAILS_PAGE_ROUTE,
6667
NONEVM_BALANCE_CHECK_ROUTE,
@@ -145,6 +146,7 @@ import { MultichainAccountDetails } from '../multichain-accounts/account-details
145146
import { AddressQRCode } from '../multichain-accounts/address-qr-code';
146147
import { MultichainAccountAddressListPage } from '../multichain-accounts/multichain-account-address-list-page';
147148
import { AccountList } from '../multichain-accounts/account-list';
149+
import { AddWalletPage } from '../multichain-accounts/add-wallet-page';
148150
import { WalletDetailsPage } from '../multichain-accounts/wallet-details-page';
149151
import {
150152
getConnectingLabel,
@@ -602,6 +604,11 @@ export default function Routes() {
602604
component={MultichainAccountAddressListPage}
603605
exact
604606
/>
607+
<Authenticated
608+
path={ADD_WALLET_PAGE_ROUTE}
609+
component={AddWalletPage}
610+
exact
611+
/>
605612
<Authenticated
606613
path={`${MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE}/:id`}
607614
component={MultichainAccountDetailsPage}

0 commit comments

Comments
 (0)