diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index 38535b0f1..61a903653 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -45,7 +45,7 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-add-account"]', ); - await driver.fill('.new-account-create-form input', '2nd account'); + await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); const accountName = await driver.waitForSelector({ css: '[data-testid="account-menu-icon"]', @@ -86,7 +86,7 @@ describe('Add account', function () { await driver.clickElement( '[data-testid="multichain-account-menu-add-account"]', ); - await driver.fill('.new-account-create-form input', '2nd account'); + await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); await waitForAccountRendered(driver); @@ -190,8 +190,7 @@ describe('Add account', function () { await driver.clickElement( '[data-testid="multichain-account-menu-add-account"]', ); - - await driver.fill('.new-account-create-form input', '2nd account'); + await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); // Wait for 2nd account to be created @@ -220,7 +219,10 @@ describe('Add account', function () { await driver.clickElement('.menu__background'); await driver.clickElement({ text: 'Import account', tag: 'button' }); await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'button' }); + + await driver.clickElement( + '[data-testid="import-account-confirm-button"]', + ); // Wait for 3rd account to be created await waitForAccountRendered(driver); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index ed6066a0d..02d67b8af 100644 --- a/test/e2e/tests/from-import-ui.spec.js +++ b/test/e2e/tests/from-import-ui.spec.js @@ -86,7 +86,7 @@ describe('MetaMask Import UI', function () { await driver.clickElement({ text: 'Add account', tag: 'button' }); // set account name - await driver.fill('.new-account-create-form input', '2nd account'); + await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Create', tag: 'button' }); @@ -203,7 +203,9 @@ describe('MetaMask Import UI', function () { // enter private key await driver.findClickableElement('#private-key-box'); await driver.fill('#private-key-box', testPrivateKey1); - await driver.clickElement({ text: 'Import', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-account-confirm-button"]', + ); // should show the correct account name const importedAccountName = await driver.findElement( @@ -230,8 +232,9 @@ describe('MetaMask Import UI', function () { // enter private key await driver.findClickableElement('#private-key-box'); await driver.fill('#private-key-box', testPrivateKey2); - await driver.findClickableElement({ text: 'Import', tag: 'button' }); - await driver.clickElement({ text: 'Import', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-account-confirm-button"]', + ); // should see new account in account menu const importedAccount2Name = await driver.findElement( @@ -303,8 +306,9 @@ describe('MetaMask Import UI', function () { fileInput.sendKeys(importJsonFile); await driver.fill('#json-password-box', 'foobarbazqux'); - - await driver.clickElement({ text: 'Import', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-account-confirm-button"]', + ); // should show the correct account name const importedAccountName = await driver.findElement( @@ -358,8 +362,9 @@ describe('MetaMask Import UI', function () { // enter private key await driver.findClickableElement('#private-key-box'); await driver.fill('#private-key-box', testPrivateKey); - await driver.findClickableElement({ text: 'Import', tag: 'button' }); - await driver.clickElement({ text: 'Import', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-account-confirm-button"]', + ); // error should occur await driver.waitForSelector({ diff --git a/test/e2e/user-actions-benchmark.js b/test/e2e/user-actions-benchmark.js index 32658f3d6..00a53c8d0 100644 --- a/test/e2e/user-actions-benchmark.js +++ b/test/e2e/user-actions-benchmark.js @@ -38,7 +38,7 @@ async function loadNewAccount() { await driver.clickElement( '[data-testid="multichain-account-menu-add-account"]', ); - await driver.fill('.new-account-create-form input', '2nd account'); + await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); await driver.waitForSelector({ css: '.currency-display-component__text', diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js index 1c7e6c45e..823e1d679 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -10,7 +10,7 @@ import { TextFieldSearch, Text, } from '../../component-library'; -import { AccountListItem } from '..'; +import { AccountListItem, CreateAccount, ImportAccount } from '..'; import { BLOCK_SIZES, Size, @@ -38,8 +38,6 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { - IMPORT_ACCOUNT_ROUTE, - NEW_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) CUSTODY_ACCOUNT_ROUTE, @@ -66,6 +64,7 @@ export const AccountListMenu = ({ onClose }) => { ///: END:ONLY_INCLUDE_IN const [searchQuery, setSearchQuery] = useState(''); + const [actionMode, setActionMode] = useState(''); let searchResults = accounts; if (searchQuery) { @@ -88,209 +87,244 @@ export const AccountListMenu = ({ onClose }) => { } }, [inputRef]); + let title = t('selectAnAccount'); + if (actionMode === 'add') { + title = t('addAccount'); + } else if (actionMode === 'import') { + title = t('importAccount'); + } + return ( setActionMode('')} > - - {/* Search box */} - {accounts.length > 1 ? ( - - setSearchQuery(e.target.value)} - clearButtonOnClick={() => setSearchQuery('')} - clearButtonProps={{ - size: Size.SM, - }} - /> - - ) : null} - {/* Account list block */} - - {searchResults.length === 0 && searchQuery !== '' ? ( - + { + if (confirmed) { + dispatch(toggleAccountMenu()); + } else { + setActionMode(''); + } + }} + /> + + ) : null} + {actionMode === 'import' ? ( + + { + if (confirmed) { + dispatch(toggleAccountMenu()); + } else { + setActionMode(''); + } + }} + /> + + ) : null} + {actionMode === '' ? ( + + {/* Search box */} + {accounts.length > 1 ? ( + - {t('noAccountsFound')} - + setSearchQuery(e.target.value)} + clearButtonOnClick={() => setSearchQuery('')} + clearButtonProps={{ + size: Size.SM, + }} + /> + ) : null} - {searchResults.map((account) => { - const connectedSite = connectedSites[account.address]?.find( - ({ origin }) => origin === currentTabOrigin, - ); + {/* Account list block */} + + {searchResults.length === 0 && searchQuery !== '' ? ( + + {t('noAccountsFound')} + + ) : null} + {searchResults.map((account) => { + const connectedSite = connectedSites[account.address]?.find( + ({ origin }) => origin === currentTabOrigin, + ); - return ( - { + dispatch(toggleAccountMenu()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.NavAccountSwitched, + properties: { + location: 'Main Menu', + }, + }); + dispatch(setSelectedAccount(account.address)); + }} + identity={account} + key={account.address} + selected={selectedAccount.address === account.address} + closeMenu={onClose} + connectedAvatar={connectedSite?.iconUrl} + connectedAvatarName={connectedSite?.name} + /> + ); + })} + + {/* Add / Import / Hardware */} + + + { + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.AccountAddSelected, + properties: { + account_type: MetaMetricsEventAccountType.Default, + location: 'Main Menu', + }, + }); + setActionMode('add'); + }} + data-testid="multichain-account-menu-add-account" + > + {t('addAccount')} + + + + { + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.AccountAddSelected, + properties: { + account_type: MetaMetricsEventAccountType.Imported, + location: 'Main Menu', + }, + }); + setActionMode('import'); + }} + > + {t('importAccount')} + + + + { dispatch(toggleAccountMenu()); trackEvent({ category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.NavAccountSwitched, + event: MetaMetricsEventName.AccountAddSelected, properties: { + account_type: MetaMetricsEventAccountType.Hardware, location: 'Main Menu', }, }); - dispatch(setSelectedAccount(account.address)); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + CONNECT_HARDWARE_ROUTE, + ); + } else { + history.push(CONNECT_HARDWARE_ROUTE); + } }} - identity={account} - key={account.address} - selected={selectedAccount.address === account.address} - closeMenu={onClose} - connectedAvatar={connectedSite?.iconUrl} - connectedAvatarName={connectedSite?.name} - /> - ); - })} - - {/* Add / Import / Hardware */} - - - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Default, - location: 'Main Menu', - }, - }); - history.push(NEW_ACCOUNT_ROUTE); - }} - data-testid="multichain-account-menu-add-account" - > - {t('addAccount')} - - - - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Imported, - location: 'Main Menu', - }, - }); - history.push(IMPORT_ACCOUNT_ROUTE); - }} - > - {t('importAccount')} - - - - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Hardware, - location: 'Main Menu', - }, - }); - if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser( - CONNECT_HARDWARE_ROUTE, - ); - } else { - history.push(CONNECT_HARDWARE_ROUTE); - } - }} - > - {t('hardwareWallet')} - - { - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - <> - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: - MetaMetricsEventName.UserClickedConnectCustodialAccount, - }); - if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser( - CUSTODY_ACCOUNT_ROUTE, - ); - } else { - history.push(CUSTODY_ACCOUNT_ROUTE); - } - }} - > - {t('connectCustodialAccountMenu')} - - {mmiPortfolioEnabled && ( + > + {t('hardwareWallet')} + + { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + <> { dispatch(toggleAccountMenu()); trackEvent({ category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.UserClickedPortfolioButton, + event: + MetaMetricsEventName.UserClickedConnectCustodialAccount, }); - window.open(mmiPortfolioUrl, '_blank'); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + CUSTODY_ACCOUNT_ROUTE, + ); + } else { + history.push(CUSTODY_ACCOUNT_ROUTE); + } }} > - {t('portfolioDashboard')} + {t('connectCustodialAccountMenu')} - )} - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.UserClickedCompliance, - }); - if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser( - COMPLIANCE_FEATURE_ROUTE, - ); - } else { - history.push(COMPLIANCE_FEATURE_ROUTE); - } - }} - > - {t('compliance')} - - > - ///: END:ONLY_INCLUDE_IN - } + {mmiPortfolioEnabled && ( + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: + MetaMetricsEventName.UserClickedPortfolioButton, + }); + window.open(mmiPortfolioUrl, '_blank'); + }} + > + {t('portfolioDashboard')} + + )} + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.UserClickedCompliance, + }); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + COMPLIANCE_FEATURE_ROUTE, + ); + } else { + history.push(COMPLIANCE_FEATURE_ROUTE); + } + }} + > + {t('compliance')} + + > + ///: END:ONLY_INCLUDE_IN + } + - + ) : null} ); }; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.test.js b/ui/components/multichain/account-list-menu/account-list-menu.test.js index 297ada6cb..9c5ed96cc 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.test.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.test.js @@ -4,11 +4,7 @@ import reactRouterDom from 'react-router-dom'; import { fireEvent, renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; -import { - NEW_ACCOUNT_ROUTE, - IMPORT_ACCOUNT_ROUTE, - CONNECT_HARDWARE_ROUTE, -} from '../../../helpers/constants/routes'; +import { CONNECT_HARDWARE_ROUTE } from '../../../helpers/constants/routes'; import { AccountListMenu } from '.'; const render = (props = { onClose: () => jest.fn() }) => { @@ -48,16 +44,24 @@ describe('AccountListMenu', () => { expect(getByText('Hardware wallet')).toBeInTheDocument(); }); - it('navigates to new account screen when clicked', () => { - const { getByText } = render(); + it('shows the account creation UI when Add Account is clicked', () => { + const { getByText, getByPlaceholderText } = render(); fireEvent.click(getByText('Add account')); - expect(historyPushMock).toHaveBeenCalledWith(NEW_ACCOUNT_ROUTE); + expect(getByText('Create')).toBeInTheDocument(); + expect(getByText('Cancel')).toBeInTheDocument(); + + fireEvent.click(getByText('Cancel')); + expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); }); - it('navigates to import account screen when clicked', () => { - const { getByText } = render(); + it('shows the account import UI when Import Account is clicked', () => { + const { getByText, getByPlaceholderText } = render(); fireEvent.click(getByText('Import account')); - expect(historyPushMock).toHaveBeenCalledWith(IMPORT_ACCOUNT_ROUTE); + expect(getByText('Import')).toBeInTheDocument(); + expect(getByText('Cancel')).toBeInTheDocument(); + + fireEvent.click(getByText('Cancel')); + expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); }); it('navigates to hardware wallet connection screen when clicked', () => { diff --git a/ui/components/multichain/create-account/create-account.js b/ui/components/multichain/create-account/create-account.js new file mode 100644 index 000000000..71d951176 --- /dev/null +++ b/ui/components/multichain/create-account/create-account.js @@ -0,0 +1,109 @@ +import React, { useContext, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { + ButtonPrimary, + ButtonSecondary, + FormTextField, +} from '../../component-library'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts'; +import { + getMetaMaskAccountsOrdered, + getMetaMaskIdentities, +} from '../../../selectors'; +import { addNewAccount, setAccountLabel } from '../../../store/actions'; +import { getMostRecentOverviewPage } from '../../../ducks/history/history'; +import Box from '../../ui/box/box'; +import { + MetaMetricsEventAccountType, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { Display } from '../../../helpers/constants/design-system'; + +export const CreateAccount = ({ onActionComplete }) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const history = useHistory(); + const trackEvent = useContext(MetaMetricsContext); + + const accounts = useSelector(getMetaMaskAccountsOrdered); + const identities = useSelector(getMetaMaskIdentities); + const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); + + const newAccountNumber = Object.keys(identities).length + 1; + const defaultAccountName = t('newAccountNumberName', [newAccountNumber]); + + const [newAccountName, setNewAccountName] = useState(''); + + const { isValidAccountName, errorMessage } = getAccountNameErrorMessage( + accounts, + { t }, + newAccountName, + defaultAccountName, + ); + + const onCreateAccount = async (name) => { + const newAccountAddress = await dispatch(addNewAccount()); + if (name) { + dispatch(setAccountLabel(newAccountAddress, name)); + } + }; + + return ( + { + event.preventDefault(); + + try { + await onCreateAccount(newAccountName || defaultAccountName); + onActionComplete(true); + trackEvent({ + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAdded, + properties: { + account_type: MetaMetricsEventAccountType.Default, + location: 'Home', + }, + }); + history.push(mostRecentOverviewPage); + } catch (error) { + trackEvent({ + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAddFailed, + properties: { + account_type: MetaMetricsEventAccountType.Default, + error: error.message, + }, + }); + } + }} + > + setNewAccountName(event.target.value)} + helpText={errorMessage} + error={!isValidAccountName} + /> + + onActionComplete()} block> + {t('cancel')} + + + {t('create')} + + + + ); +}; + +CreateAccount.propTypes = { + onActionComplete: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/create-account/create-account.stories.js b/ui/components/multichain/create-account/create-account.stories.js new file mode 100644 index 000000000..17e6619a4 --- /dev/null +++ b/ui/components/multichain/create-account/create-account.stories.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { CreateAccount } from '.'; + +export default { + title: 'Components/Multichain/CreateAccount', + component: CreateAccount, +}; + +export const DefaultStory = (args) => ; +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/create-account/create-account.test.js b/ui/components/multichain/create-account/create-account.test.js new file mode 100644 index 000000000..f93f013af --- /dev/null +++ b/ui/components/multichain/create-account/create-account.test.js @@ -0,0 +1,67 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import { fireEvent, renderWithProvider, waitFor } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { CreateAccount } from '.'; + +const render = (props = { onActionComplete: () => jest.fn() }) => { + const store = configureStore(mockState); + return renderWithProvider(, store); +}; + +const mockAddNewAccount = jest.fn().mockReturnValue({ type: 'TYPE' }); +const mockSetAccountLabel = jest.fn().mockReturnValue({ type: 'TYPE' }); + +jest.mock('../../../store/actions', () => ({ + addNewAccount: (...args) => mockAddNewAccount(...args), + setAccountLabel: (...args) => mockSetAccountLabel(...args), +})); + +describe('CreateAccount', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('displays account name input and suggests name', () => { + const { getByPlaceholderText } = render(); + + expect(getByPlaceholderText('Account 5')).toBeInTheDocument(); + }); + + it('fires onActionComplete when clicked', async () => { + const onActionComplete = jest.fn(); + const { getByText, getByPlaceholderText } = render({ onActionComplete }); + + const input = getByPlaceholderText('Account 5'); + const newAccountName = 'New Account Name'; + + fireEvent.change(input, { + target: { value: newAccountName }, + }); + fireEvent.click(getByText('Create')); + + await waitFor(() => expect(mockAddNewAccount).toHaveBeenCalled()); + await waitFor(() => + expect(mockSetAccountLabel).toHaveBeenCalledWith( + { type: 'TYPE' }, + newAccountName, + ), + ); + await waitFor(() => expect(onActionComplete).toHaveBeenCalled()); + }); + + it(`doesn't allow duplicate account names`, async () => { + const { getByText, getByPlaceholderText } = render(); + + const input = getByPlaceholderText('Account 5'); + const usedAccountName = 'Account 4'; + + fireEvent.change(input, { + target: { value: usedAccountName }, + }); + + const submitButton = getByText('Create'); + expect(submitButton).toHaveAttribute('disabled'); + }); +}); diff --git a/ui/components/multichain/create-account/index.js b/ui/components/multichain/create-account/index.js new file mode 100644 index 000000000..ebaafe89b --- /dev/null +++ b/ui/components/multichain/create-account/index.js @@ -0,0 +1 @@ +export { CreateAccount } from './create-account'; diff --git a/ui/pages/create-account/import-account/__snapshots__/json.test.tsx.snap b/ui/components/multichain/import-account/__snapshots__/json.test.tsx.snap similarity index 98% rename from ui/pages/create-account/import-account/__snapshots__/json.test.tsx.snap rename to ui/components/multichain/import-account/__snapshots__/json.test.tsx.snap index 4719d152c..2eec4dbbd 100644 --- a/ui/pages/create-account/import-account/__snapshots__/json.test.tsx.snap +++ b/ui/components/multichain/import-account/__snapshots__/json.test.tsx.snap @@ -49,6 +49,7 @@ exports[`Json should match snapshot 1`] = ` Import diff --git a/ui/pages/create-account/import-account/bottom-buttons.js b/ui/components/multichain/import-account/bottom-buttons.js similarity index 62% rename from ui/pages/create-account/import-account/bottom-buttons.js rename to ui/components/multichain/import-account/bottom-buttons.js index e195afe37..f56c43e39 100644 --- a/ui/pages/create-account/import-account/bottom-buttons.js +++ b/ui/components/multichain/import-account/bottom-buttons.js @@ -5,30 +5,32 @@ import { ButtonPrimary, ButtonSecondary, BUTTON_SECONDARY_SIZES, -} from '../../../components/component-library'; -import Box from '../../../components/ui/box/box'; -import { DISPLAY } from '../../../helpers/constants/design-system'; +} from '../../component-library'; +import Box from '../../ui/box/box'; +import { Display } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import * as actions from '../../../store/actions'; BottomButtons.propTypes = { importAccountFunc: PropTypes.func.isRequired, isPrimaryDisabled: PropTypes.bool.isRequired, + onActionComplete: PropTypes.func.isRequired, }; export default function BottomButtons({ importAccountFunc, isPrimaryDisabled, + onActionComplete, }) { const t = useI18nContext(); const dispatch = useDispatch(); return ( - + { dispatch(actions.hideWarning()); - window.history.back(); + onActionComplete(); }} size={BUTTON_SECONDARY_SIZES.LG} block @@ -36,9 +38,19 @@ export default function BottomButtons({ {t('cancel')} { + try { + const result = await importAccountFunc(); + if (result) { + onActionComplete(true); + } + } catch (e) { + // Take no action + } + }} disabled={isPrimaryDisabled} size={BUTTON_SECONDARY_SIZES.LG} + data-testid="import-account-confirm-button" block > {t('import')} diff --git a/ui/pages/create-account/import-account/bottom-buttons.stories.js b/ui/components/multichain/import-account/bottom-buttons.stories.js similarity index 84% rename from ui/pages/create-account/import-account/bottom-buttons.stories.js rename to ui/components/multichain/import-account/bottom-buttons.stories.js index 858562dfc..98665f9e7 100644 --- a/ui/pages/create-account/import-account/bottom-buttons.stories.js +++ b/ui/components/multichain/import-account/bottom-buttons.stories.js @@ -2,7 +2,7 @@ import React from 'react'; import BottomButtons from './bottom-buttons'; export default { - title: 'Pages/CreateAccount/ImportAccount/BottomButtons', + title: 'Components/Multichain/BottomButtons', component: BottomButtons, argTypes: { isPrimaryDisabled: { diff --git a/ui/pages/create-account/import-account/import-account.js b/ui/components/multichain/import-account/import-account.js similarity index 57% rename from ui/pages/create-account/import-account/import-account.js rename to ui/components/multichain/import-account/import-account.js index 4a6459130..3433b7134 100644 --- a/ui/pages/create-account/import-account/import-account.js +++ b/ui/components/multichain/import-account/import-account.js @@ -1,4 +1,5 @@ import React, { useContext, useState } from 'react'; +import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; import { MetaMetricsEventAccountImportType, @@ -6,54 +7,56 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { ButtonLink, Label, Text } from '../../../components/component-library'; -import Box from '../../../components/ui/box'; -import Dropdown from '../../../components/ui/dropdown'; +import { ButtonLink, Label, Text } from '../../component-library'; +import Box from '../../ui/box'; +import Dropdown from '../../ui/dropdown'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - BLOCK_SIZES, - BorderColor, - FONT_WEIGHT, + BlockSize, + FontWeight, JustifyContent, Size, TextVariant, } from '../../../helpers/constants/design-system'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { useRouting } from '../../../hooks/useRouting'; import * as actions from '../../../store/actions'; // Subviews import JsonImportView from './json'; import PrivateKeyImportView from './private-key'; -export default function NewAccountImportForm() { +export const ImportAccount = ({ onActionComplete }) => { const t = useI18nContext(); const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); - const { navigateToMostRecentOverviewPage } = useRouting(); const menuItems = [t('privateKey'), t('jsonFile')]; const [type, setType] = useState(menuItems[0]); - function importAccount(strategy, importArgs) { + async function importAccount(strategy, importArgs) { const loadingMessage = getLoadingMessage(strategy); - dispatch(actions.importNewAccount(strategy, importArgs, loadingMessage)) - .then(({ selectedAddress }) => { - if (selectedAddress) { - trackImportEvent(strategy, true); - dispatch(actions.hideWarning()); - navigateToMostRecentOverviewPage(); - } else { - dispatch(actions.displayWarning(t('importAccountError'))); - } - }) - .catch((error) => { - trackImportEvent(strategy, error.message); - translateWarning(error.message); - }); + try { + const { selectedAddress } = await dispatch( + actions.importNewAccount(strategy, importArgs, loadingMessage), + ); + if (selectedAddress) { + trackImportEvent(strategy, true); + dispatch(actions.hideWarning()); + onActionComplete(true); + } else { + dispatch(actions.displayWarning(t('importAccountError'))); + return false; + } + } catch (error) { + trackImportEvent(strategy, error.message); + translateWarning(error.message); + return false; + } + + return true; } function trackImportEvent(strategy, wasSuccessful) { @@ -79,13 +82,14 @@ export default function NewAccountImportForm() { function getLoadingMessage(strategy) { if (strategy === 'JSON File') { return ( - - - {t('importAccountJsonLoading1')} - - - {t('importAccountJsonLoading2')} - + <> + + {t('importAccountJsonLoading1')} + + + {t('importAccountJsonLoading2')} + + > ); } @@ -112,40 +116,22 @@ export default function NewAccountImportForm() { } } - function PrivateKeyOrJson() { - switch (type) { - case menuItems[0]: - return ; - case menuItems[1]: - default: - return ; - } - } - return ( <> - - {t('importAccount')} - - {t('importAccountMsg')}{' '} - - {t('here')} - - - - - + + {t('importAccountMsg')}{' '} + + {t('here')} + + + @@ -159,8 +145,22 @@ export default function NewAccountImportForm() { }} /> - + {type === menuItems[0] ? ( + + ) : ( + + )} > ); -} +}; + +ImportAccount.propTypes = { + onActionComplete: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/import-account/import-account.stories.js b/ui/components/multichain/import-account/import-account.stories.js new file mode 100644 index 000000000..8c96d666f --- /dev/null +++ b/ui/components/multichain/import-account/import-account.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { ImportAccount } from '.'; + +export default { + title: 'Components/Multichain/ImportAccount', + component: ImportAccount, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/import-account/index.js b/ui/components/multichain/import-account/index.js new file mode 100644 index 000000000..cd25538f4 --- /dev/null +++ b/ui/components/multichain/import-account/index.js @@ -0,0 +1 @@ +export { ImportAccount } from './import-account'; diff --git a/ui/pages/create-account/import-account/json.js b/ui/components/multichain/import-account/json.js similarity index 92% rename from ui/pages/create-account/import-account/json.js rename to ui/components/multichain/import-account/json.js index d9c0a41dd..ac14d45de 100644 --- a/ui/pages/create-account/import-account/json.js +++ b/ui/components/multichain/import-account/json.js @@ -8,7 +8,7 @@ import { Text, TEXT_FIELD_SIZES, TEXT_FIELD_TYPES, -} from '../../../components/component-library'; +} from '../../component-library'; import { Size, TextVariant, @@ -21,9 +21,13 @@ import BottomButtons from './bottom-buttons'; JsonImportSubview.propTypes = { importAccountFunc: PropTypes.func.isRequired, + onActionComplete: PropTypes.func.isRequired, }; -export default function JsonImportSubview({ importAccountFunc }) { +export default function JsonImportSubview({ + importAccountFunc, + onActionComplete, +}) { const t = useI18nContext(); const warning = useSelector((state) => state.appState.warning); const [password, setPassword] = useState(''); @@ -95,6 +99,7 @@ export default function JsonImportSubview({ importAccountFunc }) { > ); diff --git a/ui/pages/create-account/import-account/json.stories.js b/ui/components/multichain/import-account/json.stories.js similarity index 77% rename from ui/pages/create-account/import-account/json.stories.js rename to ui/components/multichain/import-account/json.stories.js index 55a579d50..ffe18cd41 100644 --- a/ui/pages/create-account/import-account/json.stories.js +++ b/ui/components/multichain/import-account/json.stories.js @@ -2,7 +2,7 @@ import React from 'react'; import JsonImportSubview from './json'; export default { - title: 'Pages/CreateAccount/ImportAccount/JsonImportSubview', + title: 'Components/Multichain/JsonImportSubview', component: JsonImportSubview, }; diff --git a/ui/pages/create-account/import-account/json.test.tsx b/ui/components/multichain/import-account/json.test.tsx similarity index 83% rename from ui/pages/create-account/import-account/json.test.tsx rename to ui/components/multichain/import-account/json.test.tsx index ebf84c724..26795e830 100644 --- a/ui/pages/create-account/import-account/json.test.tsx +++ b/ui/components/multichain/import-account/json.test.tsx @@ -7,11 +7,16 @@ import messages from '../../../../app/_locales/en/messages.json'; import Json from './json'; const mockImportFunc = jest.fn(); +const mockOnActionComplete = jest.fn(); + describe('Json', () => { const mockStore = configureMockStore()(mockState); it('should match snapshot', () => { const { asFragment } = renderWithProvider( - , + , mockStore, ); expect(asFragment()).toMatchSnapshot(); @@ -19,7 +24,10 @@ describe('Json', () => { it('should render', () => { const { getByText } = renderWithProvider( - , + , mockStore, ); @@ -29,7 +37,10 @@ describe('Json', () => { it('should import file without password', async () => { const { getByText, getByTestId } = renderWithProvider( - , + , mockStore, ); @@ -58,7 +69,10 @@ describe('Json', () => { it('should import file with password', async () => { const { getByText, getByTestId, getByPlaceholderText } = renderWithProvider( - , + , mockStore, ); diff --git a/ui/pages/create-account/import-account/private-key.js b/ui/components/multichain/import-account/private-key.js similarity index 86% rename from ui/pages/create-account/import-account/private-key.js rename to ui/components/multichain/import-account/private-key.js index 565c63114..958b4ca87 100644 --- a/ui/pages/create-account/import-account/private-key.js +++ b/ui/components/multichain/import-account/private-key.js @@ -5,15 +5,19 @@ import { FormTextField, TEXT_FIELD_SIZES, TEXT_FIELD_TYPES, -} from '../../../components/component-library'; +} from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import BottomButtons from './bottom-buttons'; PrivateKeyImportView.propTypes = { importAccountFunc: PropTypes.func.isRequired, + onActionComplete: PropTypes.func.isRequired, }; -export default function PrivateKeyImportView({ importAccountFunc }) { +export default function PrivateKeyImportView({ + importAccountFunc, + onActionComplete, +}) { const t = useI18nContext(); const [privateKey, setPrivateKey] = useState(''); @@ -51,6 +55,7 @@ export default function PrivateKeyImportView({ importAccountFunc }) { > ); diff --git a/ui/pages/create-account/import-account/private-key.stories.js b/ui/components/multichain/import-account/private-key.stories.js similarity index 100% rename from ui/pages/create-account/import-account/private-key.stories.js rename to ui/components/multichain/import-account/private-key.stories.js diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 2fad68f03..049707c6d 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -13,3 +13,5 @@ export { NetworkListItem } from './network-list-item'; export { NetworkListMenu } from './network-list-menu'; export { ProductTour } from './product-tour-popover'; export { AccountDetails } from './account-details'; +export { CreateAccount } from './create-account'; +export { ImportAccount } from './import-account'; diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 5d0f1c2f3..ffc05aebe 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -25,8 +25,6 @@ const RESTORE_VAULT_ROUTE = '/restore-vault'; const IMPORT_TOKEN_ROUTE = '/import-token'; const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token'; const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'; -const NEW_ACCOUNT_ROUTE = '/new-account'; -const IMPORT_ACCOUNT_ROUTE = '/new-account/import'; const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody'; @@ -129,8 +127,6 @@ const PATH_NAME_MAP = { [IMPORT_TOKEN_ROUTE]: 'Import Token Page', [CONFIRM_IMPORT_TOKEN_ROUTE]: 'Confirm Import Token Page', [CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE]: 'Confirm Add Suggested Token Page', - [NEW_ACCOUNT_ROUTE]: 'New Account Page', - [IMPORT_ACCOUNT_ROUTE]: 'Import Account Page', [CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page', ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) [INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional Features Done Page', @@ -194,8 +190,6 @@ export { IMPORT_TOKEN_ROUTE, CONFIRM_IMPORT_TOKEN_ROUTE, CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, - NEW_ACCOUNT_ROUTE, - IMPORT_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE, SEND_ROUTE, TOKEN_DETAILS, diff --git a/ui/pages/create-account/create-account.component.js b/ui/pages/create-account/create-account.component.js index f5219baf8..789ebb42f 100644 --- a/ui/pages/create-account/create-account.component.js +++ b/ui/pages/create-account/create-account.component.js @@ -4,8 +4,6 @@ import Box from '../../components/ui/box'; import { CONNECT_HARDWARE_ROUTE, - IMPORT_ACCOUNT_ROUTE, - NEW_ACCOUNT_ROUTE, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) CUSTODY_ACCOUNT_ROUTE, ///: END:ONLY_INCLUDE_IN @@ -14,23 +12,11 @@ import { import CustodyPage from '../institutional/custody'; ///: END:ONLY_INCLUDE_IN import ConnectHardwareForm from './connect-hardware'; -import NewAccountImportForm from './import-account'; -import NewAccountCreateForm from './new-account.container'; export default function CreateAccountPage() { return ( - - ; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/create-account/import-account/index.js b/ui/pages/create-account/import-account/index.js deleted file mode 100644 index 6b2c1d592..000000000 --- a/ui/pages/create-account/import-account/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './import-account'; diff --git a/ui/pages/create-account/index.scss b/ui/pages/create-account/index.scss deleted file mode 100644 index e423e2b53..000000000 --- a/ui/pages/create-account/index.scss +++ /dev/null @@ -1,83 +0,0 @@ -@import 'connect-hardware/index'; - -.new-account { - width: 375px; - background-color: var(--color-background-default); - box-shadow: var(--shadow-size-xs) var(--color-shadow-default); - z-index: 25; - height: unset; - overflow: auto; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - - @include screen-sm-min { - position: absolute; - } - - .bottom-border-1px { - border-width: 0 0 1px 0; - } -} - - -.new-account-create-form { - display: flex; - flex-flow: column; - align-items: center; - padding: 30px; - - &__input-label { - @include Paragraph; - - color: var(--color-text-alternative); - align-self: flex-start; - } - - &__input { - @include Paragraph; - - height: 54px; - width: 315.84px; - border: 1px solid var(--color-border-muted); - border-radius: 4px; - background-color: var(--color-background-default); - color: var(--color-text-default); - margin-top: 15px; - padding: 0 20px; - - &__error { - border: 1px solid var(--color-error-alternative); - } - - &::placeholder { - color: var(--color-text-muted); - opacity: 1; - } - } - - &__error { - @include H7; - - left: 8px; - color: var(--color-error-default); - } - - &__error-amount { - margin-top: 5px; - } - - &__buttons { - margin-top: 22px; - display: flex; - width: 100%; - justify-content: space-between; - } - - &__button { - width: 150px; - min-width: initial; - } -} diff --git a/ui/pages/create-account/new-account.component.js b/ui/pages/create-account/new-account.component.js deleted file mode 100644 index bb01e116c..000000000 --- a/ui/pages/create-account/new-account.component.js +++ /dev/null @@ -1,119 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import Button from '../../components/ui/button'; -import { - MetaMetricsEventAccountType, - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../shared/constants/metametrics'; -import { getAccountNameErrorMessage } from '../../helpers/utils/accounts'; - -export default class NewAccountCreateForm extends Component { - static defaultProps = { - newAccountNumber: 0, - }; - - state = { - newAccountName: '', - defaultAccountName: this.context.t('newAccountNumberName', [ - this.props.newAccountNumber, - ]), - }; - - render() { - const { newAccountName, defaultAccountName } = this.state; - const { history, createAccount, mostRecentOverviewPage, accounts } = - this.props; - - const createClick = (event) => { - event.preventDefault(); - createAccount(newAccountName || defaultAccountName) - .then(() => { - this.context.trackEvent({ - category: MetaMetricsEventCategory.Accounts, - event: MetaMetricsEventName.AccountAdded, - properties: { - account_type: MetaMetricsEventAccountType.Default, - location: 'Home', - }, - }); - history.push(mostRecentOverviewPage); - }) - .catch((e) => { - this.context.trackEvent({ - category: MetaMetricsEventCategory.Accounts, - event: MetaMetricsEventName.AccountAddFailed, - properties: { - account_type: MetaMetricsEventAccountType.Default, - error: e.message, - }, - }); - }); - }; - - const { isValidAccountName, errorMessage } = getAccountNameErrorMessage( - accounts, - this.context, - newAccountName, - defaultAccountName, - ); - - return ( - - - {this.context.t('accountName')} - - - - this.setState({ newAccountName: event.target.value }) - } - autoFocus - /> - - {errorMessage} - - - history.push(mostRecentOverviewPage)} - > - {this.context.t('cancel')} - - - {this.context.t('create')} - - - - - ); - } -} - -NewAccountCreateForm.propTypes = { - createAccount: PropTypes.func, - newAccountNumber: PropTypes.number, - history: PropTypes.object, - mostRecentOverviewPage: PropTypes.string.isRequired, - accounts: PropTypes.array, -}; - -NewAccountCreateForm.contextTypes = { - t: PropTypes.func, - trackEvent: PropTypes.func, -}; diff --git a/ui/pages/create-account/new-account.container.js b/ui/pages/create-account/new-account.container.js deleted file mode 100644 index 2ccb5e1cf..000000000 --- a/ui/pages/create-account/new-account.container.js +++ /dev/null @@ -1,36 +0,0 @@ -import { connect } from 'react-redux'; -import * as actions from '../../store/actions'; -import { getMostRecentOverviewPage } from '../../ducks/history/history'; -import { getMetaMaskAccountsOrdered } from '../../selectors'; -import NewAccountCreateForm from './new-account.component'; - -const mapStateToProps = (state) => { - const { - metamask: { identities = {} }, - } = state; - const numberOfExistingAccounts = Object.keys(identities).length; - const newAccountNumber = numberOfExistingAccounts + 1; - - return { - newAccountNumber, - mostRecentOverviewPage: getMostRecentOverviewPage(state), - accounts: getMetaMaskAccountsOrdered(state), - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - createAccount: (newAccountName) => { - return dispatch(actions.addNewAccount()).then((newAccountAddress) => { - if (newAccountName) { - dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)); - } - }); - }, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(NewAccountCreateForm); diff --git a/ui/pages/create-account/new-account.stories.js b/ui/pages/create-account/new-account.stories.js deleted file mode 100644 index 11fb8b543..000000000 --- a/ui/pages/create-account/new-account.stories.js +++ /dev/null @@ -1,22 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import NewAccountCreateForm from './new-account.component'; - -export default { - title: 'Pages/CreateAccount/NewAccount', - component: NewAccountCreateForm, - argTypes: { - accounts: { - control: 'array', - }, - }, - args: { - accounts: [], - }, -}; - -export const DefaultStory = (args) => ( - -); - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 41be4f458..039f0edbe 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -11,7 +11,6 @@ @import 'connected-sites/index'; @import 'connected-accounts/index'; @import 'connected-sites/index'; -@import 'create-account/index'; @import "institutional/connect-custody/index"; @import "institutional/custody/index"; @import "institutional/institutional-entity-done-page/index"; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 740247e88..33c05dd72 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -22,7 +22,6 @@ import ImportTokenPage from '../import-token'; import AddNftPage from '../add-nft'; import ConfirmImportTokenPage from '../confirm-import-token'; import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token'; -import CreateAccountPage from '../create-account'; import Loading from '../../components/ui/loading-screen'; import LoadingNetwork from '../../components/app/loading-network-screen'; import { Modal } from '../../components/app/modals'; @@ -63,7 +62,6 @@ import { CONNECT_ROUTE, DEFAULT_ROUTE, LOCK_ROUTE, - NEW_ACCOUNT_ROUTE, RESTORE_VAULT_ROUTE, REVEAL_SEED_ROUTE, SEND_ROUTE, @@ -326,7 +324,6 @@ export default class Routes extends Component { { ///: END:ONLY_INCLUDE_IN } -