mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Upgrading the Import Account modal (#17763)
Co-authored-by: georgewrmarshall <george.marshall@consensys.net> Co-authored-by: NidhiKJha <nidhi.kumari@consensys.net> Co-authored-by: montelaidev <monte.lai@consensys.net>
This commit is contained in:
parent
01abfb3ec8
commit
694773f17a
17
app/_locales/en/messages.json
generated
17
app/_locales/en/messages.json
generated
@ -1640,8 +1640,23 @@
|
||||
"importAccountError": {
|
||||
"message": "Error importing account."
|
||||
},
|
||||
"importAccountErrorIsSRP": {
|
||||
"message": "You have entered a Secret Recovery Phrase (or mnemonic). To import an account here, you have to enter a private key, which is a hexadecimal string of length 64."
|
||||
},
|
||||
"importAccountErrorNotAValidPrivateKey": {
|
||||
"message": "This is not a valid private key. You have entered a hexadecimal string, but it must be 64 characters long."
|
||||
},
|
||||
"importAccountErrorNotHexadecimal": {
|
||||
"message": "This is not a valid private key. You must enter a hexadecimal string of length 64."
|
||||
},
|
||||
"importAccountJsonLoading1": {
|
||||
"message": "Expect this JSON import to take a few minutes and freeze MetaMask."
|
||||
},
|
||||
"importAccountJsonLoading2": {
|
||||
"message": "We apologize, and we will make it faster in the future."
|
||||
},
|
||||
"importAccountMsg": {
|
||||
"message": "Imported accounts will not be associated with your originally created MetaMask account Secret Recovery Phrase. Learn more about imported accounts"
|
||||
"message": "Imported accounts won’t be associated with your MetaMask Secret Recovery Phrase. Learn more about imported accounts"
|
||||
},
|
||||
"importMyWallet": {
|
||||
"message": "Import my wallet"
|
||||
|
@ -1,9 +1,15 @@
|
||||
import log from 'loglevel';
|
||||
import { isValidMnemonic } from '@ethersproject/hdnode';
|
||||
import {
|
||||
bufferToHex,
|
||||
getBinarySize,
|
||||
isValidPrivate,
|
||||
toBuffer,
|
||||
} from 'ethereumjs-util';
|
||||
import Wallet from 'ethereumjs-wallet';
|
||||
import importers from 'ethereumjs-wallet/thirdparty';
|
||||
import { toBuffer, isValidPrivate, bufferToHex } from 'ethereumjs-util';
|
||||
import { addHexPrefix } from '../lib/util';
|
||||
import log from 'loglevel';
|
||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
||||
import { addHexPrefix } from '../lib/util';
|
||||
|
||||
const accountImporter = {
|
||||
async importAccount(strategy, args) {
|
||||
@ -15,18 +21,37 @@ const accountImporter = {
|
||||
strategies: {
|
||||
'Private Key': (privateKey) => {
|
||||
if (!privateKey) {
|
||||
throw new Error('Cannot import an empty key.');
|
||||
throw new Error('Cannot import an empty key.'); // It should never get here, because this should be stopped in the UI
|
||||
}
|
||||
|
||||
const prefixed = addHexPrefix(privateKey);
|
||||
const buffer = toBuffer(prefixed);
|
||||
|
||||
if (!isValidPrivate(buffer)) {
|
||||
throw new Error('Cannot import invalid private key.');
|
||||
// Check if the user has entered an SRP by mistake instead of a private key
|
||||
if (isValidMnemonic(privateKey.trim())) {
|
||||
throw new Error(`t('importAccountErrorIsSRP')`);
|
||||
}
|
||||
|
||||
const stripped = stripHexPrefix(prefixed);
|
||||
return stripped;
|
||||
const trimmedPrivateKey = privateKey.replace(/\s+/gu, ''); // Remove all whitespace
|
||||
|
||||
const prefixedPrivateKey = addHexPrefix(trimmedPrivateKey);
|
||||
let buffer;
|
||||
try {
|
||||
buffer = toBuffer(prefixedPrivateKey);
|
||||
} catch (e) {
|
||||
throw new Error(`t('importAccountErrorNotHexadecimal')`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
!isValidPrivate(buffer) ||
|
||||
getBinarySize(prefixedPrivateKey) !== 64 + '0x'.length // Fixes issue #17719 -- isValidPrivate() will let a key of 63 hex digits through without complaining, this line ensures 64 hex digits + '0x' = 66 digits
|
||||
) {
|
||||
throw new Error(`t('importAccountErrorNotAValidPrivateKey')`);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`t('importAccountErrorNotAValidPrivateKey')`);
|
||||
}
|
||||
|
||||
const strippedPrivateKey = stripHexPrefix(prefixedPrivateKey);
|
||||
return strippedPrivateKey;
|
||||
},
|
||||
'JSON File': (input, password) => {
|
||||
let wallet;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
|
||||
@ -94,7 +93,7 @@ describe('Account Tracker', () => {
|
||||
currentBlockGasLimit: '',
|
||||
};
|
||||
|
||||
assert.deepEqual(newAccounts, expectedAccounts);
|
||||
expect(newAccounts).toStrictEqual(expectedAccounts);
|
||||
});
|
||||
|
||||
it('should not change accounts if the passed address is not in accounts', async () => {
|
||||
@ -118,7 +117,7 @@ describe('Account Tracker', () => {
|
||||
currentBlockGasLimit: '',
|
||||
};
|
||||
|
||||
assert.deepEqual(newAccounts, expectedAccounts);
|
||||
expect(newAccounts).toStrictEqual(expectedAccounts);
|
||||
});
|
||||
|
||||
it('should update the passed address account balance, and set other balances to null, if useMultiAccountBalanceChecker is false', async () => {
|
||||
@ -137,7 +136,7 @@ describe('Account Tracker', () => {
|
||||
currentBlockGasLimit: '',
|
||||
};
|
||||
|
||||
assert.deepEqual(newAccounts, expectedAccounts);
|
||||
expect(newAccounts).toStrictEqual(expectedAccounts);
|
||||
});
|
||||
});
|
||||
|
||||
@ -164,7 +163,7 @@ describe('Account Tracker', () => {
|
||||
currentBlockGasLimit: '',
|
||||
};
|
||||
|
||||
assert.deepEqual(newAccounts, expectedAccounts);
|
||||
expect(newAccounts).toStrictEqual(expectedAccounts);
|
||||
});
|
||||
|
||||
it('should update all balances if useMultiAccountBalanceChecker is true', async () => {
|
||||
@ -192,7 +191,7 @@ describe('Account Tracker', () => {
|
||||
currentBlockGasLimit: '',
|
||||
};
|
||||
|
||||
assert.deepEqual(newAccounts, expectedAccounts);
|
||||
expect(newAccounts).toStrictEqual(expectedAccounts);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -242,7 +242,7 @@ describe('Add account', function () {
|
||||
|
||||
// enter private key',
|
||||
await driver.fill('#private-key-box', testPrivateKey);
|
||||
await driver.clickElement({ text: 'Import', tag: 'button' });
|
||||
await driver.clickElement({ text: 'Import', tag: 'span' });
|
||||
|
||||
// should show the correct account name
|
||||
const importedAccountName = await driver.findElement(
|
||||
|
@ -212,7 +212,7 @@ describe('MetaMask Import UI', function () {
|
||||
|
||||
// enter private key',
|
||||
await driver.fill('#private-key-box', testPrivateKey1);
|
||||
await driver.clickElement({ text: 'Import', tag: 'button' });
|
||||
await driver.clickElement({ text: 'Import', tag: 'span' });
|
||||
|
||||
// should show the correct account name
|
||||
const importedAccountName = await driver.findElement(
|
||||
@ -239,7 +239,7 @@ describe('MetaMask Import UI', function () {
|
||||
await driver.clickElement({ text: 'Import account', tag: 'div' });
|
||||
// enter private key
|
||||
await driver.fill('#private-key-box', testPrivateKey2);
|
||||
await driver.clickElement({ text: 'Import', tag: 'button' });
|
||||
await driver.clickElement({ text: 'Import', tag: 'span' });
|
||||
|
||||
// should see new account in account menu
|
||||
const importedAccount2Name = await driver.findElement(
|
||||
@ -315,7 +315,7 @@ describe('MetaMask Import UI', function () {
|
||||
await driver.clickElement('.account-menu__icon');
|
||||
await driver.clickElement({ text: 'Import account', tag: 'div' });
|
||||
|
||||
await driver.clickElement('.new-account-import-form__select');
|
||||
await driver.clickElement('.dropdown__select');
|
||||
await driver.clickElement({ text: 'JSON File', tag: 'option' });
|
||||
|
||||
const fileInput = await driver.findElement('input[type="file"]');
|
||||
@ -330,7 +330,7 @@ describe('MetaMask Import UI', function () {
|
||||
|
||||
await driver.fill('#json-password-box', 'foobarbazqux');
|
||||
|
||||
await driver.clickElement({ text: 'Import', tag: 'button' });
|
||||
await driver.clickElement({ text: 'Import', tag: 'span' });
|
||||
|
||||
// should show the correct account name
|
||||
const importedAccountName = await driver.findElement(
|
||||
@ -392,11 +392,11 @@ describe('MetaMask Import UI', function () {
|
||||
|
||||
// enter private key',
|
||||
await driver.fill('#private-key-box', testPrivateKey);
|
||||
await driver.clickElement({ text: 'Import', tag: 'button' });
|
||||
await driver.clickElement({ text: 'Import', tag: 'span' });
|
||||
|
||||
// error should occur
|
||||
await driver.waitForSelector({
|
||||
css: '.error',
|
||||
css: '.mm-help-text',
|
||||
text: 'The account you are trying to import is a duplicate',
|
||||
});
|
||||
},
|
||||
|
@ -144,7 +144,7 @@ const AssetListItem = ({
|
||||
name={ICON_NAMES.ARROW_RIGHT}
|
||||
color={Color.iconDefault}
|
||||
size={ICON_SIZES.SM}
|
||||
style={{ 'vertical-align': 'middle' }}
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
{sendTokenButton}
|
||||
</Box>
|
||||
|
@ -103,7 +103,6 @@ export const FormTextField = ({
|
||||
/>
|
||||
{helpText && (
|
||||
<HelpText
|
||||
error={error}
|
||||
severity={error && SEVERITIES.DANGER}
|
||||
marginTop={1}
|
||||
{...helpTextProps}
|
||||
|
@ -47,7 +47,7 @@ export const HelpText = ({
|
||||
};
|
||||
HelpText.propTypes = {
|
||||
/**
|
||||
* The color of the HelpText will be overridden if error is true
|
||||
* The color of the HelpText will be overridden if there is a severity passed
|
||||
* Defaults to Color.textDefault
|
||||
*/
|
||||
color: PropTypes.oneOf(Object.values(TextColor)),
|
||||
|
@ -26,7 +26,7 @@ export { Label } from './label';
|
||||
export { PickerNetwork } from './picker-network';
|
||||
export { Tag } from './tag';
|
||||
export { TagUrl } from './tag-url';
|
||||
export { Text, TEXT_DIRECTIONS } from './text';
|
||||
export { Text, TEXT_DIRECTIONS, INVISIBLE_CHARACTER } from './text';
|
||||
export { Input, INPUT_TYPES } from './input';
|
||||
export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field';
|
||||
export { TextFieldSearch } from './text-field-search';
|
||||
|
@ -1,2 +1,2 @@
|
||||
export { Text } from './text';
|
||||
export { TEXT_DIRECTIONS } from './text.constants';
|
||||
export { TEXT_DIRECTIONS, INVISIBLE_CHARACTER } from './text.constants';
|
||||
|
@ -3,3 +3,9 @@ export const TEXT_DIRECTIONS = {
|
||||
RIGHT_TO_LEFT: 'rtl',
|
||||
AUTO: 'auto',
|
||||
};
|
||||
|
||||
/**
|
||||
* The INVISIBLE_CHARACTER is a very useful tool if you want to make sure a line of text
|
||||
* takes up vertical space even if it's empty.
|
||||
*/
|
||||
export const INVISIBLE_CHARACTER = '\u200B';
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { INVISIBLE_CHARACTER } from '../../components/component-library';
|
||||
|
||||
export function getAccountNameErrorMessage(
|
||||
accounts,
|
||||
context,
|
||||
@ -26,7 +28,7 @@ export function getAccountNameErrorMessage(
|
||||
|
||||
let errorMessage;
|
||||
if (isValidAccountName) {
|
||||
errorMessage = '\u200d'; // This is Unicode for an invisible character, so the spacing stays constant
|
||||
errorMessage = INVISIBLE_CHARACTER; // Using an invisible character, so the spacing stays constant
|
||||
} else if (isDuplicateAccountName) {
|
||||
errorMessage = context.t('accountNameDuplicate');
|
||||
} else if (isReservedAccountName) {
|
||||
|
@ -1,20 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import Box from '../../components/ui/box';
|
||||
|
||||
import {
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
IMPORT_ACCOUNT_ROUTE,
|
||||
CONNECT_HARDWARE_ROUTE,
|
||||
IMPORT_ACCOUNT_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
} from '../../helpers/constants/routes';
|
||||
import NewAccountCreateForm from './new-account.container';
|
||||
import NewAccountImportForm from './import-account';
|
||||
import ConnectHardwareForm from './connect-hardware';
|
||||
import NewAccountImportForm from './import-account';
|
||||
import NewAccountCreateForm from './new-account.container';
|
||||
|
||||
export default class CreateAccountPage extends Component {
|
||||
render() {
|
||||
export default function CreateAccountPage() {
|
||||
return (
|
||||
<div className="new-account">
|
||||
<div className="new-account__form">
|
||||
<Box className="new-account">
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
@ -32,8 +31,6 @@ export default class CreateAccountPage extends Component {
|
||||
component={ConnectHardwareForm}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
48
ui/pages/create-account/import-account/bottom-buttons.js
Normal file
48
ui/pages/create-account/import-account/bottom-buttons.js
Normal file
@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
ButtonPrimary,
|
||||
ButtonSecondary,
|
||||
BUTTON_SECONDARY_SIZES,
|
||||
} from '../../../components/component-library';
|
||||
import Box from '../../../components/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,
|
||||
};
|
||||
|
||||
export default function BottomButtons({
|
||||
importAccountFunc,
|
||||
isPrimaryDisabled,
|
||||
}) {
|
||||
const t = useI18nContext();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<Box display={DISPLAY.FLEX} gap={4}>
|
||||
<ButtonSecondary
|
||||
onClick={() => {
|
||||
dispatch(actions.hideWarning());
|
||||
window.history.back();
|
||||
}}
|
||||
size={BUTTON_SECONDARY_SIZES.LG}
|
||||
block
|
||||
>
|
||||
{t('cancel')}
|
||||
</ButtonSecondary>
|
||||
<ButtonPrimary
|
||||
onClick={importAccountFunc}
|
||||
disabled={isPrimaryDisabled}
|
||||
size={BUTTON_SECONDARY_SIZES.LG}
|
||||
block
|
||||
>
|
||||
{t('import')}
|
||||
</ButtonPrimary>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import BottomButtons from './bottom-buttons';
|
||||
|
||||
export default {
|
||||
title: 'Pages/CreateAccount/ImportAccount/BottomButtons',
|
||||
component: BottomButtons,
|
||||
argTypes: {
|
||||
isPrimaryDisabled: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <BottomButtons {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
159
ui/pages/create-account/import-account/import-account.js
Normal file
159
ui/pages/create-account/import-account/import-account.js
Normal file
@ -0,0 +1,159 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EVENT, EVENT_NAMES } 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 { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||
import {
|
||||
BLOCK_SIZES,
|
||||
BorderColor,
|
||||
FONT_WEIGHT,
|
||||
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() {
|
||||
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) {
|
||||
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')));
|
||||
trackImportEvent(strategy, false);
|
||||
}
|
||||
})
|
||||
.catch((error) => translateWarning(error.message));
|
||||
}
|
||||
|
||||
function trackImportEvent(strategy, wasSuccessful) {
|
||||
const accountImportType =
|
||||
strategy === 'Private Key'
|
||||
? EVENT.ACCOUNT_IMPORT_TYPES.PRIVATE_KEY
|
||||
: EVENT.ACCOUNT_IMPORT_TYPES.JSON;
|
||||
|
||||
const event = wasSuccessful
|
||||
? EVENT_NAMES.ACCOUNT_ADDED
|
||||
: EVENT_NAMES.ACCOUNT_ADD_FAILED;
|
||||
|
||||
trackEvent({
|
||||
category: EVENT.CATEGORIES.ACCOUNTS,
|
||||
event,
|
||||
properties: {
|
||||
account_type: EVENT.ACCOUNT_TYPES.IMPORTED,
|
||||
account_import_type: accountImportType,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getLoadingMessage(strategy) {
|
||||
if (strategy === 'JSON File') {
|
||||
return (
|
||||
<Text width={BLOCK_SIZES.THREE_FOURTHS} fontWeight={FONT_WEIGHT.BOLD}>
|
||||
<br />
|
||||
{t('importAccountJsonLoading1')}
|
||||
<br />
|
||||
<br />
|
||||
{t('importAccountJsonLoading2')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message - an Error/Warning message caught in importAccount()
|
||||
* This function receives a message that is a string like:
|
||||
* `t('importAccountErrorNotHexadecimal')`
|
||||
* `t('importAccountErrorIsSRP')`
|
||||
* `t('importAccountErrorNotAValidPrivateKey')`
|
||||
* and feeds it through useI18nContext
|
||||
*/
|
||||
function translateWarning(message) {
|
||||
if (message && !message.startsWith('t(')) {
|
||||
// This is just a normal error message
|
||||
dispatch(actions.displayWarning(message));
|
||||
} else {
|
||||
// This is an error message in a form like
|
||||
// `t('importAccountErrorNotHexadecimal')`
|
||||
// so slice off the first 3 chars and last 2 chars, and feed to i18n
|
||||
dispatch(actions.displayWarning(t(message.slice(3, -2))));
|
||||
}
|
||||
}
|
||||
|
||||
function PrivateKeyOrJson() {
|
||||
switch (type) {
|
||||
case menuItems[0]:
|
||||
return <PrivateKeyImportView importAccountFunc={importAccount} />;
|
||||
case menuItems[1]:
|
||||
default:
|
||||
return <JsonImportView importAccountFunc={importAccount} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
padding={4}
|
||||
className="bottom-border-1px" // There is no way to do just a bottom border in the Design System
|
||||
borderColor={BorderColor.borderDefault}
|
||||
>
|
||||
<Text variant={TextVariant.headingLg}>{t('importAccount')}</Text>
|
||||
<Text variant={TextVariant.bodySm} marginTop={2}>
|
||||
{t('importAccountMsg')}{' '}
|
||||
<ButtonLink
|
||||
size={Size.inherit}
|
||||
href={ZENDESK_URLS.IMPORTED_ACCOUNTS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('here')}
|
||||
</ButtonLink>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box padding={4} paddingBottom={8} paddingLeft={4} paddingRight={4}>
|
||||
<Label
|
||||
width={BLOCK_SIZES.FULL}
|
||||
marginBottom={4}
|
||||
justifyContent={JustifyContent.spaceBetween}
|
||||
>
|
||||
{t('selectType')}
|
||||
<Dropdown
|
||||
options={menuItems.map((text) => ({ value: text }))}
|
||||
selectedOption={type}
|
||||
onChange={(value) => {
|
||||
dispatch(actions.hideWarning());
|
||||
setType(value);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<PrivateKeyOrJson />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import NewAccountImportForm from '.';
|
||||
|
||||
export default {
|
||||
title: 'Pages/CreateAccount/ImportAccount',
|
||||
component: NewAccountImportForm,
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <NewAccountImportForm {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -1,79 +1 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Dropdown from '../../../components/ui/dropdown';
|
||||
|
||||
// Subviews
|
||||
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
||||
import JsonImportView from './json';
|
||||
import PrivateKeyImportView from './private-key';
|
||||
|
||||
export default class AccountImportSubview extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
getMenuItemTexts() {
|
||||
return [this.context.t('privateKey'), this.context.t('jsonFile')];
|
||||
}
|
||||
|
||||
renderImportView() {
|
||||
const { type } = this.state;
|
||||
const menuItems = this.getMenuItemTexts();
|
||||
const current = type || menuItems[0];
|
||||
|
||||
switch (current) {
|
||||
case this.context.t('privateKey'):
|
||||
return <PrivateKeyImportView />;
|
||||
case this.context.t('jsonFile'):
|
||||
return <JsonImportView />;
|
||||
default:
|
||||
return <JsonImportView />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const menuItems = this.getMenuItemTexts();
|
||||
const { type } = this.state;
|
||||
const { t } = this.context;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-container__header">
|
||||
<div className="page-container__title">{t('importAccount')}</div>
|
||||
<div className="page-container__subtitle">
|
||||
{t('importAccountMsg')}
|
||||
<span
|
||||
className="new-account-info-link"
|
||||
onClick={() => {
|
||||
global.platform.openTab({
|
||||
url: ZENDESK_URLS.IMPORTED_ACCOUNTS,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('here')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="new-account-import-form">
|
||||
<div className="new-account-import-form__select-section">
|
||||
<div className="new-account-import-form__select-label">
|
||||
{t('selectType')}
|
||||
</div>
|
||||
<Dropdown
|
||||
className="new-account-import-form__select"
|
||||
options={menuItems.map((text) => ({ value: text }))}
|
||||
selectedOption={type || menuItems[0]}
|
||||
onChange={(value) => {
|
||||
this.setState({ type: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{this.renderImportView()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
export { default } from './import-account';
|
||||
|
@ -1,90 +0,0 @@
|
||||
.new-account-info-link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: var(--color-primary-default);
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.new-account-import-form {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
padding: 0 30px 30px;
|
||||
|
||||
@include screen-sm-max {
|
||||
padding: 0 22px 22px;
|
||||
}
|
||||
|
||||
&__select-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 29px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__select-label {
|
||||
@include Paragraph;
|
||||
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: 210px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__private-key-password-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__instruction {
|
||||
@include Paragraph;
|
||||
|
||||
color: var(--color-text-muted);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&__private-key {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
&__help-link {
|
||||
color: var(--color-primary-default);
|
||||
}
|
||||
|
||||
&__input-password {
|
||||
@include Paragraph;
|
||||
|
||||
height: 54px;
|
||||
width: 315px;
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-background-default);
|
||||
color: var(--color-text-default);
|
||||
margin-top: 16px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&__json {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
margin-top: 29px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin-top: 39px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
@ -1,189 +1,97 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import FileInput from 'react-simple-file-input';
|
||||
import * as actions from '../../../store/actions';
|
||||
import { getMetaMaskAccounts } from '../../../selectors';
|
||||
import Button from '../../../components/ui/button';
|
||||
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
|
||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||
import {
|
||||
ButtonLink,
|
||||
FormTextField,
|
||||
Text,
|
||||
TEXT_FIELD_SIZES,
|
||||
TEXT_FIELD_TYPES,
|
||||
} from '../../../components/component-library';
|
||||
import {
|
||||
Size,
|
||||
TextVariant,
|
||||
TEXT_ALIGN,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { displayWarning } from '../../../store/actions';
|
||||
import BottomButtons from './bottom-buttons';
|
||||
|
||||
class JsonImportSubview extends Component {
|
||||
state = {
|
||||
fileContents: '',
|
||||
isEmpty: true,
|
||||
};
|
||||
JsonImportSubview.propTypes = {
|
||||
importAccountFunc: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
inputRef = React.createRef();
|
||||
export default function JsonImportSubview({ importAccountFunc }) {
|
||||
const t = useI18nContext();
|
||||
const warning = useSelector((state) => state.appState.warning);
|
||||
const [password, setPassword] = useState('');
|
||||
const [fileContents, setFileContents] = useState('');
|
||||
|
||||
render() {
|
||||
const { error, history, mostRecentOverviewPage } = this.props;
|
||||
const enabled = !this.state.isEmpty && this.state.fileContents !== '';
|
||||
const isPrimaryDisabled = password === '' || fileContents === '';
|
||||
|
||||
function handleKeyPress(event) {
|
||||
if (!isPrimaryDisabled && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
_importAccountFunc();
|
||||
}
|
||||
}
|
||||
|
||||
function _importAccountFunc() {
|
||||
if (isPrimaryDisabled) {
|
||||
displayWarning(t('needImportFile'));
|
||||
} else {
|
||||
importAccountFunc('JSON File', [fileContents, password]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="new-account-import-form__json">
|
||||
<p>{this.context.t('usedByClients')}</p>
|
||||
<a
|
||||
className="new-account-import-form__help-link"
|
||||
<>
|
||||
<Text variant={TextVariant.bodyMd} textAlign={TEXT_ALIGN.CENTER}>
|
||||
{t('usedByClients')}
|
||||
<ButtonLink
|
||||
size={Size.inherit}
|
||||
href={ZENDESK_URLS.IMPORTED_ACCOUNTS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{this.context.t('fileImportFail')}
|
||||
</a>
|
||||
{t('fileImportFail')}
|
||||
</ButtonLink>
|
||||
</Text>
|
||||
|
||||
<FileInput
|
||||
readAs="text"
|
||||
onLoad={this.onLoad.bind(this)}
|
||||
onLoad={(event) => setFileContents(event.target.result)}
|
||||
style={{
|
||||
padding: '20px 0px 12px 15%',
|
||||
fontSize: '15px',
|
||||
fontSize: '16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="new-account-import-form__input-password"
|
||||
type="password"
|
||||
placeholder={this.context.t('enterPassword')}
|
||||
|
||||
<FormTextField
|
||||
id="json-password-box"
|
||||
onKeyPress={this.createKeyringOnEnter.bind(this)}
|
||||
onChange={() => this.checkInputEmpty()}
|
||||
ref={this.inputRef}
|
||||
size={TEXT_FIELD_SIZES.LARGE}
|
||||
autoFocus
|
||||
type={TEXT_FIELD_TYPES.PASSWORD}
|
||||
helpText={warning}
|
||||
error
|
||||
placeholder={t('enterPassword')}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
inputProps={{
|
||||
onKeyPress: handleKeyPress,
|
||||
}}
|
||||
marginBottom={4}
|
||||
/>
|
||||
<div className="new-account-create-form__buttons">
|
||||
<Button
|
||||
type="secondary"
|
||||
large
|
||||
className="new-account-create-form__button"
|
||||
onClick={() => history.push(mostRecentOverviewPage)}
|
||||
>
|
||||
{this.context.t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
large
|
||||
className="new-account-create-form__button"
|
||||
onClick={() => this.createNewKeychain()}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{this.context.t('import')}
|
||||
</Button>
|
||||
</div>
|
||||
{error ? <span className="error">{error}</span> : null}
|
||||
</div>
|
||||
|
||||
<BottomButtons
|
||||
importAccountFunc={_importAccountFunc}
|
||||
isPrimaryDisabled={isPrimaryDisabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
onLoad(event) {
|
||||
this.setState({
|
||||
fileContents: event.target.result,
|
||||
});
|
||||
}
|
||||
|
||||
createKeyringOnEnter(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.createNewKeychain();
|
||||
}
|
||||
}
|
||||
|
||||
createNewKeychain() {
|
||||
const {
|
||||
firstAddress,
|
||||
displayWarning,
|
||||
history,
|
||||
importNewJsonAccount,
|
||||
mostRecentOverviewPage,
|
||||
setSelectedAddress,
|
||||
} = this.props;
|
||||
const { fileContents } = this.state;
|
||||
const { t } = this.context;
|
||||
|
||||
if (!fileContents) {
|
||||
const message = t('needImportFile');
|
||||
displayWarning(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.inputRef.current.value;
|
||||
|
||||
importNewJsonAccount([fileContents, password])
|
||||
.then(({ selectedAddress }) => {
|
||||
if (selectedAddress) {
|
||||
history.push(mostRecentOverviewPage);
|
||||
this.context.trackEvent({
|
||||
category: EVENT.CATEGORIES.ACCOUNTS,
|
||||
event: EVENT_NAMES.ACCOUNT_ADDED,
|
||||
properties: {
|
||||
account_type: EVENT.ACCOUNT_TYPES.IMPORTED,
|
||||
account_import_type: EVENT.ACCOUNT_IMPORT_TYPES.JSON,
|
||||
},
|
||||
});
|
||||
displayWarning(null);
|
||||
} else {
|
||||
displayWarning(t('importAccountError'));
|
||||
this.context.trackEvent({
|
||||
category: EVENT.CATEGORIES.ACCOUNTS,
|
||||
event: EVENT_NAMES.ACCOUNT_ADD_FAILED,
|
||||
properties: {
|
||||
account_type: EVENT.ACCOUNT_TYPES.IMPORTED,
|
||||
account_import_type: EVENT.ACCOUNT_IMPORT_TYPES.JSON,
|
||||
},
|
||||
});
|
||||
setSelectedAddress(firstAddress);
|
||||
}
|
||||
})
|
||||
.catch((err) => err && displayWarning(err.message || err));
|
||||
}
|
||||
|
||||
checkInputEmpty() {
|
||||
const password = this.inputRef.current.value;
|
||||
let isEmpty = true;
|
||||
if (password !== '') {
|
||||
isEmpty = false;
|
||||
}
|
||||
this.setState({ isEmpty });
|
||||
}
|
||||
}
|
||||
|
||||
JsonImportSubview.propTypes = {
|
||||
error: PropTypes.string,
|
||||
displayWarning: PropTypes.func,
|
||||
firstAddress: PropTypes.string,
|
||||
importNewJsonAccount: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
setSelectedAddress: PropTypes.func,
|
||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
error: state.appState.warning,
|
||||
firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
displayWarning: (warning) => dispatch(actions.displayWarning(warning)),
|
||||
importNewJsonAccount: (options) =>
|
||||
dispatch(actions.importNewAccount('JSON File', options)),
|
||||
setSelectedAddress: (address) =>
|
||||
dispatch(actions.setSelectedAddress(address)),
|
||||
};
|
||||
};
|
||||
|
||||
JsonImportSubview.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
trackEvent: PropTypes.func,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(JsonImportSubview);
|
||||
|
11
ui/pages/create-account/import-account/json.stories.js
Normal file
11
ui/pages/create-account/import-account/json.stories.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import JsonImportSubview from './json';
|
||||
|
||||
export default {
|
||||
title: 'Pages/CreateAccount/ImportAccount/JsonImportSubview',
|
||||
component: JsonImportSubview,
|
||||
};
|
||||
|
||||
export const DefaultStory = () => <JsonImportSubview />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -1,160 +1,57 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../../../store/actions';
|
||||
import { getMetaMaskAccounts } from '../../../selectors';
|
||||
import Button from '../../../components/ui/button';
|
||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
FormTextField,
|
||||
TEXT_FIELD_SIZES,
|
||||
TEXT_FIELD_TYPES,
|
||||
} from '../../../components/component-library';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import BottomButtons from './bottom-buttons';
|
||||
|
||||
class PrivateKeyImportView extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
trackEvent: PropTypes.func,
|
||||
};
|
||||
PrivateKeyImportView.propTypes = {
|
||||
importAccountFunc: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
importNewAccount: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
displayWarning: PropTypes.func.isRequired,
|
||||
setSelectedAddress: PropTypes.func.isRequired,
|
||||
firstAddress: PropTypes.string.isRequired,
|
||||
error: PropTypes.node,
|
||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||
};
|
||||
export default function PrivateKeyImportView({ importAccountFunc }) {
|
||||
const t = useI18nContext();
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
|
||||
inputRef = React.createRef();
|
||||
const warning = useSelector((state) => state.appState.warning);
|
||||
|
||||
state = { isEmpty: true };
|
||||
|
||||
createNewKeychain() {
|
||||
const privateKey = this.inputRef.current.value;
|
||||
const {
|
||||
importNewAccount,
|
||||
history,
|
||||
displayWarning,
|
||||
mostRecentOverviewPage,
|
||||
setSelectedAddress,
|
||||
firstAddress,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
importNewAccount('Private Key', [privateKey])
|
||||
.then(({ selectedAddress }) => {
|
||||
if (selectedAddress) {
|
||||
this.context.trackEvent({
|
||||
category: EVENT.CATEGORIES.ACCOUNTS,
|
||||
event: EVENT_NAMES.ACCOUNT_ADDED,
|
||||
properties: {
|
||||
account_type: EVENT.ACCOUNT_TYPES.IMPORTED,
|
||||
account_import_type: EVENT.ACCOUNT_IMPORT_TYPES.PRIVATE_KEY,
|
||||
},
|
||||
});
|
||||
history.push(mostRecentOverviewPage);
|
||||
displayWarning(null);
|
||||
} else {
|
||||
displayWarning(t('importAccountError'));
|
||||
this.context.trackEvent({
|
||||
category: EVENT.CATEGORIES.ACCOUNTS,
|
||||
event: EVENT_NAMES.ACCOUNT_ADD_FAILED,
|
||||
properties: {
|
||||
account_type: EVENT.ACCOUNT_TYPES.IMPORTED,
|
||||
account_import_type: EVENT.ACCOUNT_IMPORT_TYPES.PRIVATE_KEY,
|
||||
},
|
||||
});
|
||||
setSelectedAddress(firstAddress);
|
||||
}
|
||||
})
|
||||
.catch((err) => err && displayWarning(err.message || err));
|
||||
}
|
||||
|
||||
createKeyringOnEnter = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
function handleKeyPress(event) {
|
||||
if (privateKey !== '' && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.createNewKeychain();
|
||||
_importAccountFunc();
|
||||
}
|
||||
};
|
||||
|
||||
checkInputEmpty() {
|
||||
const privateKey = this.inputRef.current.value;
|
||||
let isEmpty = true;
|
||||
if (privateKey !== '') {
|
||||
isEmpty = false;
|
||||
}
|
||||
this.setState({ isEmpty });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, displayWarning } = this.props;
|
||||
function _importAccountFunc() {
|
||||
importAccountFunc('Private Key', [privateKey]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="new-account-import-form__private-key">
|
||||
<span className="new-account-import-form__instruction">
|
||||
{this.context.t('pastePrivateKey')}
|
||||
</span>
|
||||
<div className="new-account-import-form__private-key-password-container">
|
||||
<input
|
||||
className="new-account-import-form__input-password"
|
||||
type="password"
|
||||
<>
|
||||
<FormTextField
|
||||
id="private-key-box"
|
||||
onKeyPress={(e) => this.createKeyringOnEnter(e)}
|
||||
onChange={() => this.checkInputEmpty()}
|
||||
ref={this.inputRef}
|
||||
size={TEXT_FIELD_SIZES.LARGE}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="new-account-import-form__buttons">
|
||||
<Button
|
||||
type="secondary"
|
||||
large
|
||||
className="new-account-create-form__button"
|
||||
onClick={() => {
|
||||
const { history, mostRecentOverviewPage } = this.props;
|
||||
displayWarning(null);
|
||||
history.push(mostRecentOverviewPage);
|
||||
type={TEXT_FIELD_TYPES.PASSWORD}
|
||||
helpText={warning}
|
||||
error
|
||||
label={t('pastePrivateKey')}
|
||||
value={privateKey}
|
||||
onChange={(event) => setPrivateKey(event.target.value)}
|
||||
inputProps={{
|
||||
onKeyPress: handleKeyPress,
|
||||
}}
|
||||
>
|
||||
{this.context.t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
large
|
||||
className="new-account-create-form__button"
|
||||
onClick={() => this.createNewKeychain()}
|
||||
disabled={this.state.isEmpty}
|
||||
>
|
||||
{this.context.t('import')}
|
||||
</Button>
|
||||
</div>
|
||||
{error ? <span className="error">{error}</span> : null}
|
||||
</div>
|
||||
marginBottom={4}
|
||||
/>
|
||||
|
||||
<BottomButtons
|
||||
importAccountFunc={_importAccountFunc}
|
||||
isPrimaryDisabled={privateKey === ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(PrivateKeyImportView);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
error: state.appState.warning,
|
||||
firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
importNewAccount: (strategy, [privateKey]) => {
|
||||
return dispatch(actions.importNewAccount(strategy, [privateKey]));
|
||||
},
|
||||
displayWarning: (message) =>
|
||||
dispatch(actions.displayWarning(message || null)),
|
||||
setSelectedAddress: (address) =>
|
||||
dispatch(actions.setSelectedAddress(address)),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PrivateKeyImportView from './private-key';
|
||||
|
||||
export default {
|
||||
title: 'Pages/CreateAccount/ImportAccount/PrivateKeyImportView',
|
||||
component: PrivateKeyImportView,
|
||||
argTypes: {
|
||||
importAccountFunc: {
|
||||
control: {
|
||||
type: 'function',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <PrivateKeyImportView {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -1,5 +1,4 @@
|
||||
@import 'connect-hardware/index';
|
||||
@import 'import-account/index';
|
||||
|
||||
.new-account {
|
||||
width: 375px;
|
||||
@ -18,47 +17,8 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include H2;
|
||||
|
||||
color: var(--color-text-alternative);
|
||||
font-weight: 500;
|
||||
margin-top: 22px;
|
||||
margin-left: 29px;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
margin-left: 22px;
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
|
||||
&__tab {
|
||||
@include H4;
|
||||
|
||||
height: 54px;
|
||||
padding: 15px 10px;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__tab:hover {
|
||||
color: var(--color-text-default);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__selected {
|
||||
color: var(--color-primary-default);
|
||||
border-bottom: 3px solid var(--color-primary-default);
|
||||
cursor: initial;
|
||||
pointer-events: none;
|
||||
}
|
||||
.bottom-border-1px {
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
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',
|
||||
@ -13,10 +14,9 @@ export default {
|
||||
accounts: [],
|
||||
},
|
||||
};
|
||||
export const DefaultStory = (args) => {
|
||||
return (
|
||||
|
||||
export const DefaultStory = (args) => (
|
||||
<NewAccountCreateForm {...args} createAccount={action('Account Created')} />
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
@ -350,9 +350,11 @@ describe('Actions', () => {
|
||||
_setBackgroundConnection(background);
|
||||
|
||||
await store.dispatch(
|
||||
actions.importNewAccount('Private Key', [
|
||||
'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3',
|
||||
]),
|
||||
actions.importNewAccount(
|
||||
'Private Key',
|
||||
['c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'],
|
||||
'',
|
||||
),
|
||||
);
|
||||
expect(importAccountWithStrategy.callCount).toStrictEqual(1);
|
||||
});
|
||||
@ -369,9 +371,8 @@ describe('Actions', () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'SHOW_LOADING_INDICATION',
|
||||
payload: 'This may take a while, please be patient.',
|
||||
payload: undefined,
|
||||
},
|
||||
{ type: 'DISPLAY_WARNING', payload: 'error' },
|
||||
{ type: 'HIDE_LOADING_INDICATION' },
|
||||
];
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user