AccountCreate updates (#3988)
* Add esjify for mocha + ejs * First pass through, intl + basic smoketests * Create store * Update for renames * Pass store around * createType into store * Move stage into store * Update labels * Define stages * address into store * Add @observer * Retrieve name from store * Store phrase in store * isWindowsPhrase into store * gethAddresses to store * Store manages geth addresses * passwordHint into store * Fix build * rawKey into store * import json files * name set direct from component * No parent change callbacks * canCreate from store * createAccounts into store * expand create tests * Windows phrase testcases * Properly bind newError * FirstRun use of new CreateAccount * Add fix & test for selectedAddress match * Call into store from props * onChangeIdentity fix & test * Phrase set fix & test * RecoveryPhrase tested manually (issues addressed via tests) * Hex import manual test (& tests added for errors) * New eslint update fixes * grumble: set default type from store (with test) * grumble: pass copy of accounts (observable injection) * grumble: Summary owners can be array or array-like
This commit is contained in:
parent
153f2ca2f2
commit
06433033d9
@ -14,33 +14,54 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Form, Input, InputAddress } from '~/ui';
|
import { Form, Input, InputAddress } from '~/ui';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AccountDetails extends Component {
|
export default class AccountDetails extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
address: PropTypes.string,
|
store: PropTypes.object.isRequired
|
||||||
name: PropTypes.string,
|
|
||||||
phrase: PropTypes.string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { address, name } = this.props;
|
const { address, name } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
|
||||||
allowCopy
|
allowCopy
|
||||||
hint='a descriptive name for the account'
|
hint={
|
||||||
label='account name'
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetails.name.hint'
|
||||||
|
defaultMessage='a descriptive name for the account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetails.name.label'
|
||||||
|
defaultMessage='account name'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
value={ name }
|
value={ name }
|
||||||
/>
|
/>
|
||||||
<InputAddress
|
<InputAddress
|
||||||
disabled
|
disabled
|
||||||
hint='the network address for the account'
|
hint={
|
||||||
label='address'
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetails.address.hint'
|
||||||
|
defaultMessage='the network address for the account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetails.address.label'
|
||||||
|
defaultMessage='address'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value={ address }
|
value={ address }
|
||||||
/>
|
/>
|
||||||
{ this.renderPhrase() }
|
{ this.renderPhrase() }
|
||||||
@ -49,7 +70,7 @@ export default class AccountDetails extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPhrase () {
|
renderPhrase () {
|
||||||
const { phrase } = this.props;
|
const { phrase } = this.props.store;
|
||||||
|
|
||||||
if (!phrase) {
|
if (!phrase) {
|
||||||
return null;
|
return null;
|
||||||
@ -57,10 +78,20 @@ export default class AccountDetails extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
|
||||||
allowCopy
|
allowCopy
|
||||||
hint='the account recovery phrase'
|
hint={
|
||||||
label='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)'
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetails.phrase.hint'
|
||||||
|
defaultMessage='the account recovery phrase'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetails.phrase.label'
|
||||||
|
defaultMessage='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
value={ phrase }
|
value={ phrase }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import AccountDetails from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<AccountDetails
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/AccountDetails', () => {
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,9 +14,10 @@
|
|||||||
/* You should have received a copy of the GNU General Public License
|
/* You should have received a copy of the GNU General Public License
|
||||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.address {
|
.address {
|
||||||
color: #999;
|
color: #999;
|
||||||
padding-top: 1em;
|
|
||||||
line-height: 1.618em;
|
line-height: 1.618em;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
|
padding-top: 1em;
|
||||||
}
|
}
|
||||||
|
@ -14,29 +14,46 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import styles from './accountDetailsGeth.css';
|
import styles from './accountDetailsGeth.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AccountDetailsGeth extends Component {
|
export default class AccountDetailsGeth extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
addresses: PropTypes.array
|
store: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { addresses } = this.props;
|
const { gethAddresses } = this.props.store;
|
||||||
|
|
||||||
const formatted = addresses.map((address, idx) => {
|
|
||||||
const comma = !idx ? '' : ((idx === addresses.length - 1) ? ' & ' : ', ');
|
|
||||||
|
|
||||||
return `${comma}${address}`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>You have imported { addresses.length } addresses from the Geth keystore:</div>
|
<div>
|
||||||
<div className={ styles.address }>{ formatted }</div>
|
<FormattedMessage
|
||||||
|
id='createAccount.accountDetailsGeth.imported'
|
||||||
|
defaultMessage='You have imported {number} addresses from the Geth keystore:'
|
||||||
|
values={ {
|
||||||
|
number: gethAddresses.length
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={ styles.address }>
|
||||||
|
{ this.formatAddresses(gethAddresses) }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatAddresses (addresses) {
|
||||||
|
return addresses.map((address, index) => {
|
||||||
|
const comma = !index
|
||||||
|
? ''
|
||||||
|
: ((index === addresses.length - 1) ? ' & ' : ', ');
|
||||||
|
|
||||||
|
return `${comma}${address}`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import AccountDetailsGeth from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<AccountDetailsGeth
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/AccountDetailsGeth', () => {
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('utility', () => {
|
||||||
|
describe('formatAddresses', () => {
|
||||||
|
let instance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
instance = component.instance();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a single item', () => {
|
||||||
|
expect(instance.formatAddresses(['one'])).to.equal('one');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders multiple items', () => {
|
||||||
|
expect(instance.formatAddresses(['one', 'two', 'three'])).to.equal('one, two & three');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,50 +14,81 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||||
|
|
||||||
import styles from '../createAccount.css';
|
import styles from '../createAccount.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class CreationType extends Component {
|
export default class CreationType extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired
|
store: PropTypes.object.isRequired
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount () {
|
|
||||||
this.props.onChange('fromNew');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { createType } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.spaced }>
|
<div className={ styles.spaced }>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
defaultSelected='fromNew'
|
defaultSelected={ createType }
|
||||||
name='creationType'
|
name='creationType'
|
||||||
onChange={ this.onChange }
|
onChange={ this.onChange }
|
||||||
>
|
>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label='Create new account manually'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.creationType.fromNew.label'
|
||||||
|
defaultMessage='Create new account manually'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value='fromNew'
|
value='fromNew'
|
||||||
/>
|
/>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label='Recover account from recovery phrase'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.creationType.fromPhrase.label'
|
||||||
|
defaultMessage='Recover account from recovery phrase'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value='fromPhrase'
|
value='fromPhrase'
|
||||||
/>
|
/>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label='Import accounts from Geth keystore'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.creationType.fromGeth.label'
|
||||||
|
defaultMessage='Import accounts from Geth keystore'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value='fromGeth'
|
value='fromGeth'
|
||||||
/>
|
/>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label='Import account from a backup JSON file'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.creationType.fromJSON.label'
|
||||||
|
defaultMessage='Import account from a backup JSON file'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value='fromJSON'
|
value='fromJSON'
|
||||||
/>
|
/>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label='Import account from an Ethereum pre-sale wallet'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.creationType.fromPresale.label'
|
||||||
|
defaultMessage='Import account from an Ethereum pre-sale wallet'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value='fromPresale'
|
value='fromPresale'
|
||||||
/>
|
/>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label='Import raw private key'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.creationType.fromRaw.label'
|
||||||
|
defaultMessage='Import raw private key'
|
||||||
|
/>
|
||||||
|
}
|
||||||
value='fromRaw'
|
value='fromRaw'
|
||||||
/>
|
/>
|
||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
@ -66,6 +97,8 @@ export default class CreationType extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChange = (event) => {
|
onChange = (event) => {
|
||||||
this.props.onChange(event.target.value);
|
const { store } = this.props;
|
||||||
|
|
||||||
|
store.setCreateType(event.target.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import CreationType from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<CreationType
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/CreationType', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selector', () => {
|
||||||
|
const SELECT_TYPE = 'fromRaw';
|
||||||
|
let selector;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType(SELECT_TYPE);
|
||||||
|
selector = component.find('RadioButtonGroup');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the selector', () => {
|
||||||
|
expect(selector.get(0)).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the store type to defaultSelected', () => {
|
||||||
|
expect(selector.props().defaultSelected).to.equal(SELECT_TYPE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onChange', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.instance().onChange({ target: { value: 'testing' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes the store createType', () => {
|
||||||
|
expect(store.createType).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,86 +14,113 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { IconButton } from 'material-ui';
|
import { IconButton } from 'material-ui';
|
||||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||||
import ActionAutorenew from 'material-ui/svg-icons/action/autorenew';
|
|
||||||
|
|
||||||
import { Form, Input, IdentityIcon } from '~/ui';
|
import { Form, Input, IdentityIcon, PasswordStrength } from '~/ui';
|
||||||
|
import { RefreshIcon } from '~/ui/Icons';
|
||||||
import ERRORS from '../errors';
|
|
||||||
|
|
||||||
import styles from '../createAccount.css';
|
import styles from '../createAccount.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class CreateAccount extends Component {
|
export default class CreateAccount extends Component {
|
||||||
static contextTypes = {
|
static propTypes = {
|
||||||
api: PropTypes.object.isRequired,
|
newError: PropTypes.func.isRequired,
|
||||||
store: PropTypes.object.isRequired
|
store: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
accountName: '',
|
|
||||||
accountNameError: ERRORS.noName,
|
|
||||||
accounts: null,
|
accounts: null,
|
||||||
isValidName: false,
|
|
||||||
isValidPass: true,
|
|
||||||
passwordHint: '',
|
|
||||||
password1: '',
|
|
||||||
password1Error: null,
|
|
||||||
password2: '',
|
|
||||||
password2Error: null,
|
|
||||||
selectedAddress: ''
|
selectedAddress: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.createIdentities();
|
return this.createIdentities();
|
||||||
this.props.onChange(false, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error } = this.state;
|
const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Input
|
<Input
|
||||||
label='account name'
|
error={ nameError }
|
||||||
hint='a descriptive name for the account'
|
hint={
|
||||||
error={ accountNameError }
|
<FormattedMessage
|
||||||
value={ accountName }
|
id='createAccount.newAccount.name.hint'
|
||||||
|
defaultMessage='a descriptive name for the account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newAccount.name.label'
|
||||||
|
defaultMessage='account name'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditAccountName }
|
onChange={ this.onEditAccountName }
|
||||||
|
value={ name }
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='password hint'
|
hint={
|
||||||
hint='(optional) a hint to help with remembering the password'
|
<FormattedMessage
|
||||||
value={ passwordHint }
|
id='createAccount.newAccount.hint.hint'
|
||||||
|
defaultMessage='(optional) a hint to help with remembering the password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newAccount.hint.label'
|
||||||
|
defaultMessage='password hint'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditPasswordHint }
|
onChange={ this.onEditPasswordHint }
|
||||||
|
value={ passwordHint }
|
||||||
/>
|
/>
|
||||||
<div className={ styles.passwords }>
|
<div className={ styles.passwords }>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password'
|
hint={
|
||||||
hint='a strong, unique password'
|
<FormattedMessage
|
||||||
|
id='createAccount.newAccount.password.hint'
|
||||||
|
defaultMessage='a strong, unique password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newAccount.password.label'
|
||||||
|
defaultMessage='password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditPassword }
|
||||||
type='password'
|
type='password'
|
||||||
error={ password1Error }
|
value={ password }
|
||||||
value={ password1 }
|
|
||||||
onChange={ this.onEditPassword1 }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password (repeat)'
|
error={ passwordRepeatError }
|
||||||
hint='verify your password'
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newAccount.password2.hint'
|
||||||
|
defaultMessage='verify your password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newAccount.password2.label'
|
||||||
|
defaultMessage='password (repeat)'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditPasswordRepeat }
|
||||||
type='password'
|
type='password'
|
||||||
error={ password2Error }
|
value={ passwordRepeat }
|
||||||
value={ password2 }
|
|
||||||
onChange={ this.onEditPassword2 }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PasswordStrength input={ password } />
|
||||||
{ this.renderIdentitySelector() }
|
{ this.renderIdentitySelector() }
|
||||||
{ this.renderIdentities() }
|
{ this.renderIdentities() }
|
||||||
</Form>
|
</Form>
|
||||||
@ -107,7 +134,9 @@ export default class CreateAccount extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons = Object.keys(accounts).map((address) => {
|
const buttons = Object
|
||||||
|
.keys(accounts)
|
||||||
|
.map((address) => {
|
||||||
return (
|
return (
|
||||||
<RadioButton
|
<RadioButton
|
||||||
className={ styles.button }
|
className={ styles.button }
|
||||||
@ -119,10 +148,10 @@ export default class CreateAccount extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
valueSelected={ selectedAddress }
|
|
||||||
className={ styles.selector }
|
className={ styles.selector }
|
||||||
name='identitySelector'
|
name='identitySelector'
|
||||||
onChange={ this.onChangeIdentity }
|
onChange={ this.onChangeIdentity }
|
||||||
|
valueSelected={ selectedAddress }
|
||||||
>
|
>
|
||||||
{ buttons }
|
{ buttons }
|
||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
@ -136,7 +165,9 @@ export default class CreateAccount extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identities = Object.keys(accounts).map((address) => {
|
const identities = Object
|
||||||
|
.keys(accounts)
|
||||||
|
.map((address) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ styles.identity }
|
className={ styles.identity }
|
||||||
@ -155,12 +186,8 @@ export default class CreateAccount extends Component {
|
|||||||
<div className={ styles.identities }>
|
<div className={ styles.identities }>
|
||||||
{ identities }
|
{ identities }
|
||||||
<div className={ styles.refresh }>
|
<div className={ styles.refresh }>
|
||||||
<IconButton
|
<IconButton onTouchTap={ this.createIdentities }>
|
||||||
onTouchTap={ this.createIdentities }
|
<RefreshIcon color='rgb(0, 151, 167)' />
|
||||||
>
|
|
||||||
<ActionAutorenew
|
|
||||||
color='rgb(0, 151, 167)'
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -168,122 +195,64 @@ export default class CreateAccount extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createIdentities = () => {
|
createIdentities = () => {
|
||||||
const { api } = this.context;
|
const { store } = this.props;
|
||||||
|
|
||||||
Promise
|
return store
|
||||||
.all([
|
.createIdentities()
|
||||||
api.parity.generateSecretPhrase(),
|
.then((accounts) => {
|
||||||
api.parity.generateSecretPhrase(),
|
const selectedAddress = Object.keys(accounts)[0];
|
||||||
api.parity.generateSecretPhrase(),
|
const { phrase } = accounts[selectedAddress];
|
||||||
api.parity.generateSecretPhrase(),
|
|
||||||
api.parity.generateSecretPhrase()
|
|
||||||
])
|
|
||||||
.then((phrases) => {
|
|
||||||
return Promise
|
|
||||||
.all(phrases.map((phrase) => api.parity.phraseToAddress(phrase)))
|
|
||||||
.then((addresses) => {
|
|
||||||
const accounts = {};
|
|
||||||
|
|
||||||
phrases.forEach((phrase, idx) => {
|
store.setAddress(selectedAddress);
|
||||||
accounts[addresses[idx]] = {
|
store.setPhrase(phrase);
|
||||||
address: addresses[idx],
|
|
||||||
phrase: phrase
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedAddress: addresses[0],
|
accounts,
|
||||||
accounts: accounts
|
selectedAddress
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('createIdentities', error);
|
this.props.newError(error);
|
||||||
setTimeout(this.createIdentities, 1000);
|
|
||||||
this.newError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParent = () => {
|
|
||||||
const { isValidName, isValidPass, accounts, accountName, passwordHint, password1, selectedAddress } = this.state;
|
|
||||||
const isValid = isValidName && isValidPass;
|
|
||||||
|
|
||||||
this.props.onChange(isValid, {
|
|
||||||
address: selectedAddress,
|
|
||||||
name: accountName,
|
|
||||||
passwordHint,
|
|
||||||
password: password1,
|
|
||||||
phrase: accounts[selectedAddress].phrase
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeIdentity = (event) => {
|
onChangeIdentity = (event) => {
|
||||||
const address = event.target.value || event.target.getAttribute('value');
|
const { store } = this.props;
|
||||||
|
const selectedAddress = event.target.value || event.target.getAttribute('value');
|
||||||
|
|
||||||
if (!address) {
|
if (!selectedAddress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({ selectedAddress }, () => {
|
||||||
selectedAddress: address
|
const { phrase } = this.state.accounts[selectedAddress];
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPasswordHint = (event, passwordHint) => {
|
store.setAddress(selectedAddress);
|
||||||
this.setState({
|
store.setPhrase(phrase);
|
||||||
passwordHint
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditAccountName = (event) => {
|
onEditPasswordHint = (event, passwordHint) => {
|
||||||
const accountName = event.target.value;
|
const { store } = this.props;
|
||||||
let accountNameError = null;
|
|
||||||
|
|
||||||
if (!accountName || !accountName.trim().length) {
|
store.setPasswordHint(passwordHint);
|
||||||
accountNameError = ERRORS.noName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
onEditAccountName = (event, name) => {
|
||||||
accountName,
|
const { store } = this.props;
|
||||||
accountNameError,
|
|
||||||
isValidName: !accountNameError
|
store.setName(name);
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditPassword1 = (event) => {
|
onEditPassword = (event, password) => {
|
||||||
const password1 = event.target.value;
|
const { store } = this.props;
|
||||||
let password2Error = null;
|
|
||||||
|
|
||||||
if (password1 !== this.state.password2) {
|
store.setPassword(password);
|
||||||
password2Error = ERRORS.noMatchPassword;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
onEditPasswordRepeat = (event, password) => {
|
||||||
password1,
|
const { store } = this.props;
|
||||||
password1Error: null,
|
|
||||||
password2Error,
|
|
||||||
isValidPass: !password2Error
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPassword2 = (event) => {
|
store.setPasswordRepeat(password);
|
||||||
const password2 = event.target.value;
|
|
||||||
let password2Error = null;
|
|
||||||
|
|
||||||
if (password2 !== this.state.password1) {
|
|
||||||
password2Error = ERRORS.noMatchPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
password2,
|
|
||||||
password2Error,
|
|
||||||
isValidPass: !password2Error
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
newError = (error) => {
|
|
||||||
const { store } = this.context;
|
|
||||||
|
|
||||||
store.dispatch({ type: 'newError', error });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
161
js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
Normal file
161
js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { createApi, createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import NewAccount from './';
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
api = createApi();
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<NewAccount
|
||||||
|
store={ store }
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: { api }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/NewAccount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lifecycle', () => {
|
||||||
|
describe('componentWillMount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return instance.componentWillMount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates initial accounts', () => {
|
||||||
|
expect(Object.keys(instance.state.accounts).length).to.equal(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the initial selected value', () => {
|
||||||
|
expect(instance.state.selectedAddress).to.equal(Object.keys(instance.state.accounts)[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event handlers', () => {
|
||||||
|
describe('onChangeIdentity', () => {
|
||||||
|
let address;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
address = Object.keys(instance.state.accounts)[3];
|
||||||
|
|
||||||
|
sinon.spy(store, 'setAddress');
|
||||||
|
sinon.spy(store, 'setPhrase');
|
||||||
|
instance.onChangeIdentity({ target: { value: address } });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setAddress.restore();
|
||||||
|
store.setPhrase.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the state with the new value', () => {
|
||||||
|
expect(instance.state.selectedAddress).to.equal(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the new address on the store', () => {
|
||||||
|
expect(store.setAddress).to.have.been.calledWith(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the new phrase on the store', () => {
|
||||||
|
expect(store.setPhrase).to.have.been.calledWith(instance.state.accounts[address].phrase);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPassword');
|
||||||
|
instance.onEditPassword(null, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPassword.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPassword).to.have.been.calledWith('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordRepeat', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordRepeat');
|
||||||
|
instance.onEditPasswordRepeat(null, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordRepeat.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordRepeat).to.have.been.calledWith('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordHint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordHint');
|
||||||
|
instance.onEditPasswordHint(null, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordHint.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordHint).to.have.been.calledWith('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditAccountName', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setName');
|
||||||
|
instance.onEditAccountName(null, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setName.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setName).to.have.been.calledWith('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,29 +15,28 @@
|
|||||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
.list {
|
.list {
|
||||||
}
|
input+div>div {
|
||||||
|
|
||||||
.list input+div>div {
|
|
||||||
top: 13px;
|
top: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection {
|
.selection {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
|
||||||
|
|
||||||
.selection .icon {
|
.icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection .detail {
|
.detail {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
|
||||||
|
|
||||||
.detail .address {
|
.address {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail .balance {
|
.balance {
|
||||||
font-family: 'Roboto Mono', monospace;
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,63 +14,68 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { Checkbox } from 'material-ui';
|
import { Checkbox } from 'material-ui';
|
||||||
|
|
||||||
import { IdentityIcon } from '~/ui';
|
import { IdentityIcon } from '~/ui';
|
||||||
|
|
||||||
import styles from './newGeth.css';
|
import styles from './newGeth.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class NewGeth extends Component {
|
export default class NewGeth extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accounts: PropTypes.object.isRequired,
|
store: PropTypes.object.isRequired
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
available: []
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.loadAvailable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { available } = this.state;
|
const { gethAccountsAvailable, gethAddresses } = this.props.store;
|
||||||
|
|
||||||
if (!available.length) {
|
if (!gethAccountsAvailable.length) {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.list }>There are currently no importable keys available from the Geth keystore, which are not already available on your Parity instance</div>
|
<div className={ styles.list }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newGeth.noKeys'
|
||||||
|
defaultMessage='There are currently no importable keys available from the Geth keystore, which are not already available on your Parity instance'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkboxes = available.map((account) => {
|
const checkboxes = gethAccountsAvailable.map((account) => {
|
||||||
|
const onSelect = (event) => this.onSelectAddress(event, account.address);
|
||||||
|
|
||||||
const label = (
|
const label = (
|
||||||
<div className={ styles.selection }>
|
<div className={ styles.selection }>
|
||||||
<div className={ styles.icon }>
|
<div className={ styles.icon }>
|
||||||
<IdentityIcon
|
<IdentityIcon
|
||||||
center inline
|
|
||||||
address={ account.address }
|
address={ account.address }
|
||||||
|
center
|
||||||
|
inline
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.detail }>
|
<div className={ styles.detail }>
|
||||||
<div className={ styles.address }>{ account.address }</div>
|
<div className={ styles.address }>
|
||||||
<div className={ styles.balance }>{ account.balance } ETH</div>
|
{ account.address }
|
||||||
|
</div>
|
||||||
|
<div className={ styles.balance }>
|
||||||
|
{ account.balance } ETH
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
checked={ gethAddresses.includes(account.address) }
|
||||||
key={ account.address }
|
key={ account.address }
|
||||||
checked={ account.checked }
|
|
||||||
label={ label }
|
label={ label }
|
||||||
data-address={ account.address }
|
onCheck={ onSelect }
|
||||||
onCheck={ this.onSelect }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -82,51 +87,9 @@ export default class NewGeth extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect = (event, checked) => {
|
onSelectAddress = (event, address) => {
|
||||||
const address = event.target.getAttribute('data-address');
|
const { store } = this.props;
|
||||||
|
|
||||||
if (!address) {
|
store.selectGethAccount(address);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { available } = this.state;
|
|
||||||
const account = available.find((_account) => _account.address === address);
|
|
||||||
|
|
||||||
account.checked = checked;
|
|
||||||
const selected = available.filter((_account) => _account.checked);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
available
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onChange(selected.length, selected.map((account) => account.address));
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAvailable = () => {
|
|
||||||
const { api } = this.context;
|
|
||||||
const { accounts } = this.props;
|
|
||||||
|
|
||||||
api.parity
|
|
||||||
.listGethAccounts()
|
|
||||||
.then((_addresses) => {
|
|
||||||
const addresses = (addresses || []).filter((address) => !accounts[address]);
|
|
||||||
|
|
||||||
return Promise
|
|
||||||
.all(addresses.map((address) => api.eth.getBalance(address)))
|
|
||||||
.then((balances) => {
|
|
||||||
this.setState({
|
|
||||||
available: addresses.map((address, idx) => {
|
|
||||||
return {
|
|
||||||
address,
|
|
||||||
balance: api.util.fromWei(balances[idx]).toFormat(5),
|
|
||||||
checked: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('loadAvailable', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
66
js/src/modals/CreateAccount/NewGeth/newGeth.spec.js
Normal file
66
js/src/modals/CreateAccount/NewGeth/newGeth.spec.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import NewGeth from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<NewGeth
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/NewGeth', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onSelectAddress', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'selectGethAccount');
|
||||||
|
instance.onSelectAddress(null, 'testAddress');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.selectGethAccount.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.selectGethAccount).to.have.been.calledWith('testAddress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,91 +14,114 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { FloatingActionButton } from 'material-ui';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { FloatingActionButton } from 'material-ui';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import EditorAttachFile from 'material-ui/svg-icons/editor/attach-file';
|
|
||||||
|
|
||||||
import { Form, Input } from '~/ui';
|
import { Form, Input } from '~/ui';
|
||||||
|
import { AttachFileIcon } from '~/ui/Icons';
|
||||||
import ERRORS from '../errors';
|
|
||||||
|
|
||||||
import styles from '../createAccount.css';
|
import styles from '../createAccount.css';
|
||||||
|
|
||||||
const FAKEPATH = 'C:\\fakepath\\';
|
|
||||||
const STYLE_HIDDEN = { display: 'none' };
|
const STYLE_HIDDEN = { display: 'none' };
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class NewImport extends Component {
|
export default class NewImport extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired
|
store: PropTypes.object.isRequired
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
accountName: '',
|
|
||||||
accountNameError: ERRORS.noName,
|
|
||||||
isValidFile: false,
|
|
||||||
isValidPass: true,
|
|
||||||
isValidName: false,
|
|
||||||
password: '',
|
|
||||||
passwordError: null,
|
|
||||||
passwordHint: '',
|
|
||||||
walletFile: '',
|
|
||||||
walletFileError: ERRORS.noFile,
|
|
||||||
walletJson: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount () {
|
|
||||||
this.props.onChange(false, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { name, nameError, password, passwordHint, walletFile, walletFileError } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Input
|
<Input
|
||||||
label='account name'
|
error={ nameError }
|
||||||
hint='a descriptive name for the account'
|
hint={
|
||||||
error={ this.state.accountNameError }
|
<FormattedMessage
|
||||||
value={ this.state.accountName }
|
id='createAccount.newImport.name.hint'
|
||||||
onChange={ this.onEditAccountName }
|
defaultMessage='a descriptive name for the account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newImport.name.label'
|
||||||
|
defaultMessage='account name'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditName }
|
||||||
|
value={ name }
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='password hint'
|
hint={
|
||||||
hint='(optional) a hint to help with remembering the password'
|
<FormattedMessage
|
||||||
value={ this.state.passwordHint }
|
id='createAccount.newImport.hint.hint'
|
||||||
|
defaultMessage='(optional) a hint to help with remembering the password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newImport.hint.label'
|
||||||
|
defaultMessage='password hint'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditpasswordHint }
|
onChange={ this.onEditpasswordHint }
|
||||||
|
value={ passwordHint }
|
||||||
/>
|
/>
|
||||||
<div className={ styles.passwords }>
|
<div className={ styles.passwords }>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password'
|
hint={
|
||||||
hint='the password to unlock the wallet'
|
<FormattedMessage
|
||||||
|
id='createAccount.newImport.password.hint'
|
||||||
|
defaultMessage='the password to unlock the wallet'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newImport.password.label'
|
||||||
|
defaultMessage='password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
type='password'
|
type='password'
|
||||||
error={ this.state.passwordError }
|
|
||||||
value={ this.state.password }
|
|
||||||
onChange={ this.onEditPassword }
|
onChange={ this.onEditPassword }
|
||||||
|
value={ password }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
disabled
|
disabled
|
||||||
label='wallet file'
|
error={ walletFileError }
|
||||||
hint='the wallet file for import'
|
hint={
|
||||||
error={ this.state.walletFileError }
|
<FormattedMessage
|
||||||
value={ this.state.walletFile }
|
id='createAccount.newImport.file.hint'
|
||||||
|
defaultMessage='the wallet file for import'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.newImport.file.label'
|
||||||
|
defaultMessage='wallet file'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={ walletFile }
|
||||||
/>
|
/>
|
||||||
<div className={ styles.upload }>
|
<div className={ styles.upload }>
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
mini
|
mini
|
||||||
onTouchTap={ this.openFileDialog }
|
onTouchTap={ this.openFileDialog }
|
||||||
>
|
>
|
||||||
<EditorAttachFile />
|
<AttachFileIcon />
|
||||||
</FloatingActionButton>
|
</FloatingActionButton>
|
||||||
<input
|
<input
|
||||||
ref='fileUpload'
|
|
||||||
type='file'
|
|
||||||
style={ STYLE_HIDDEN }
|
|
||||||
onChange={ this.onFileChange }
|
onChange={ this.onFileChange }
|
||||||
|
ref='fileUpload'
|
||||||
|
style={ STYLE_HIDDEN }
|
||||||
|
type='file'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -107,73 +130,37 @@ export default class NewImport extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFileChange = (event) => {
|
onFileChange = (event) => {
|
||||||
const el = event.target;
|
const { store } = this.props;
|
||||||
const error = ERRORS.noFile;
|
|
||||||
|
|
||||||
if (el.files.length) {
|
if (event.target.files.length) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => store.setWalletJson(event.target.result);
|
||||||
this.setState({
|
reader.readAsText(event.target.files[0]);
|
||||||
walletJson: event.target.result,
|
|
||||||
walletFileError: null,
|
|
||||||
isValidFile: true
|
|
||||||
}, this.updateParent);
|
|
||||||
};
|
|
||||||
reader.readAsText(el.files[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
store.setWalletFile(event.target.value);
|
||||||
walletFile: el.value.replace(FAKEPATH, ''),
|
|
||||||
walletFileError: error,
|
|
||||||
isValidFile: false
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openFileDialog = () => {
|
openFileDialog = () => {
|
||||||
ReactDOM.findDOMNode(this.refs.fileUpload).click();
|
ReactDOM.findDOMNode(this.refs.fileUpload).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParent = () => {
|
onEditName = (event, name) => {
|
||||||
const valid = this.state.isValidName && this.state.isValidPass && this.state.isValidFile;
|
const { store } = this.props;
|
||||||
|
|
||||||
this.props.onChange(valid, {
|
store.setName(name);
|
||||||
name: this.state.accountName,
|
}
|
||||||
passwordHint: this.state.passwordHint,
|
|
||||||
password: this.state.password,
|
onEditPassword = (event, password) => {
|
||||||
phrase: null,
|
const { store } = this.props;
|
||||||
json: this.state.walletJson
|
|
||||||
});
|
store.setPassword(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditPasswordHint = (event, passwordHint) => {
|
onEditPasswordHint = (event, passwordHint) => {
|
||||||
this.setState({
|
const { store } = this.props;
|
||||||
passwordHint
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditAccountName = (event) => {
|
store.setPasswordHint(passwordHint);
|
||||||
const accountName = event.target.value;
|
|
||||||
let accountNameError = null;
|
|
||||||
|
|
||||||
if (!accountName || !accountName.trim().length) {
|
|
||||||
accountNameError = ERRORS.noName;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
accountName,
|
|
||||||
accountNameError,
|
|
||||||
isValidName: !accountNameError
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPassword = (event) => {
|
|
||||||
const password = event.target.value;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
password,
|
|
||||||
passwordError: null,
|
|
||||||
isValidPass: true
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
96
js/src/modals/CreateAccount/NewImport/newImport.spec.js
Normal file
96
js/src/modals/CreateAccount/NewImport/newImport.spec.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import NewImport from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<NewImport
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/NewImport', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onEditName', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setName');
|
||||||
|
instance.onEditName(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setName.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setName).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPassword');
|
||||||
|
instance.onEditPassword(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPassword.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPassword).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordHint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordHint');
|
||||||
|
instance.onEditPasswordHint(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordHint.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordHint).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,172 +14,152 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Form, Input } from '~/ui';
|
import { Form, Input, PasswordStrength } from '~/ui';
|
||||||
|
|
||||||
import styles from '../createAccount.css';
|
import styles from '../createAccount.css';
|
||||||
|
|
||||||
import ERRORS from '../errors';
|
@observer
|
||||||
|
|
||||||
export default class RawKey extends Component {
|
export default class RawKey extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired
|
store: PropTypes.object.isRequired
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
accountName: '',
|
|
||||||
accountNameError: ERRORS.noName,
|
|
||||||
isValidKey: false,
|
|
||||||
isValidName: false,
|
|
||||||
isValidPass: true,
|
|
||||||
passwordHint: '',
|
|
||||||
password1: '',
|
|
||||||
password1Error: null,
|
|
||||||
password2: '',
|
|
||||||
password2Error: null,
|
|
||||||
rawKey: '',
|
|
||||||
rawKeyError: ERRORS.noKey
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount () {
|
|
||||||
this.props.onChange(false, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error, rawKey, rawKeyError } = this.state;
|
const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, rawKey, rawKeyError } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Input
|
<Input
|
||||||
hint='the raw hex encoded private key'
|
|
||||||
label='private key'
|
|
||||||
error={ rawKeyError }
|
error={ rawKeyError }
|
||||||
value={ rawKey }
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.private.hint'
|
||||||
|
defaultMessage='the raw hex encoded private key'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.private.label'
|
||||||
|
defaultMessage='private key'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditKey }
|
onChange={ this.onEditKey }
|
||||||
|
value={ rawKey }
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='account name'
|
error={ nameError }
|
||||||
hint='a descriptive name for the account'
|
hint={
|
||||||
error={ accountNameError }
|
<FormattedMessage
|
||||||
value={ accountName }
|
id='createAccount.rawKey.name.hint'
|
||||||
onChange={ this.onEditAccountName }
|
defaultMessage='a descriptive name for the account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.name.label'
|
||||||
|
defaultMessage='account name'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditName }
|
||||||
|
value={ name }
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='password hint'
|
hint={
|
||||||
hint='(optional) a hint to help with remembering the password'
|
<FormattedMessage
|
||||||
value={ passwordHint }
|
id='createAccount.rawKey.hint.hint'
|
||||||
|
defaultMessage='(optional) a hint to help with remembering the password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.hint.label'
|
||||||
|
defaultMessage='password hint'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditPasswordHint }
|
onChange={ this.onEditPasswordHint }
|
||||||
|
value={ passwordHint }
|
||||||
/>
|
/>
|
||||||
<div className={ styles.passwords }>
|
<div className={ styles.passwords }>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password'
|
hint={
|
||||||
hint='a strong, unique password'
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.password.hint'
|
||||||
|
defaultMessage='a strong, unique password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.password.label'
|
||||||
|
defaultMessage='password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditPassword }
|
||||||
type='password'
|
type='password'
|
||||||
error={ password1Error }
|
value={ password }
|
||||||
value={ password1 }
|
|
||||||
onChange={ this.onEditPassword1 }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password (repeat)'
|
error={ passwordRepeatError }
|
||||||
hint='verify your password'
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.password2.hint'
|
||||||
|
defaultMessage='verify your password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.rawKey.password2.label'
|
||||||
|
defaultMessage='password (repeat)'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditPasswordRepeat }
|
||||||
type='password'
|
type='password'
|
||||||
error={ password2Error }
|
value={ passwordRepeat }
|
||||||
value={ password2 }
|
|
||||||
onChange={ this.onEditPassword2 }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PasswordStrength input={ password } />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParent = () => {
|
onEditName = (event, name) => {
|
||||||
const { isValidName, isValidPass, isValidKey, accountName, passwordHint, password1, rawKey } = this.state;
|
const { store } = this.props;
|
||||||
const isValid = isValidName && isValidPass && isValidKey;
|
|
||||||
|
|
||||||
this.props.onChange(isValid, {
|
store.setName(name);
|
||||||
name: accountName,
|
|
||||||
passwordHint,
|
|
||||||
password: password1,
|
|
||||||
rawKey
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditPasswordHint = (event, value) => {
|
onEditPasswordHint = (event, passwordHint) => {
|
||||||
this.setState({
|
const { store } = this.props;
|
||||||
passwordHint: value
|
|
||||||
});
|
store.setPasswordHint(passwordHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditKey = (event) => {
|
onEditPassword = (event, password) => {
|
||||||
const { api } = this.context;
|
const { store } = this.props;
|
||||||
const rawKey = event.target.value;
|
|
||||||
let rawKeyError = null;
|
|
||||||
|
|
||||||
if (!rawKey || !rawKey.trim().length) {
|
store.setPassword(password);
|
||||||
rawKeyError = ERRORS.noKey;
|
|
||||||
} else if (rawKey.substr(0, 2) !== '0x' || rawKey.substr(2).length !== 64 || !api.util.isHex(rawKey)) {
|
|
||||||
rawKeyError = ERRORS.invalidKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
onEditPasswordRepeat = (event, password) => {
|
||||||
rawKey,
|
const { store } = this.props;
|
||||||
rawKeyError,
|
|
||||||
isValidKey: !rawKeyError
|
store.setPasswordRepeat(password);
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditAccountName = (event) => {
|
onEditKey = (event, rawKey) => {
|
||||||
const accountName = event.target.value;
|
const { store } = this.props;
|
||||||
let accountNameError = null;
|
|
||||||
|
|
||||||
if (!accountName || !accountName.trim().length) {
|
store.setRawKey(rawKey);
|
||||||
accountNameError = ERRORS.noName;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
accountName,
|
|
||||||
accountNameError,
|
|
||||||
isValidName: !accountNameError
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPassword1 = (event) => {
|
|
||||||
const password1 = event.target.value;
|
|
||||||
let password2Error = null;
|
|
||||||
|
|
||||||
if (password1 !== this.state.password2) {
|
|
||||||
password2Error = ERRORS.noMatchPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
password1,
|
|
||||||
password1Error: null,
|
|
||||||
password2Error,
|
|
||||||
isValidPass: !password2Error
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPassword2 = (event) => {
|
|
||||||
const password2 = event.target.value;
|
|
||||||
let password2Error = null;
|
|
||||||
|
|
||||||
if (password2 !== this.state.password1) {
|
|
||||||
password2Error = ERRORS.noMatchPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
password2,
|
|
||||||
password2Error,
|
|
||||||
isValidPass: !password2Error
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
js/src/modals/CreateAccount/RawKey/rawKey.spec.js
Normal file
126
js/src/modals/CreateAccount/RawKey/rawKey.spec.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import RawKey from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<RawKey
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/RawKey', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onEditName', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setName');
|
||||||
|
instance.onEditName(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setName.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setName).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditKey', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setRawKey');
|
||||||
|
instance.onEditKey(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setRawKey.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setRawKey).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPassword');
|
||||||
|
instance.onEditPassword(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPassword.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPassword).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordRepeat', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordRepeat');
|
||||||
|
instance.onEditPasswordRepeat(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordRepeat.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordRepeat).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordHint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordHint');
|
||||||
|
instance.onEditPasswordHint(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordHint.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordHint).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,183 +14,165 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { Checkbox } from 'material-ui';
|
import { Checkbox } from 'material-ui';
|
||||||
|
|
||||||
import { Form, Input } from '~/ui';
|
import { Form, Input, PasswordStrength } from '~/ui';
|
||||||
|
|
||||||
import styles from '../createAccount.css';
|
import styles from '../createAccount.css';
|
||||||
|
|
||||||
import ERRORS from '../errors';
|
@observer
|
||||||
|
|
||||||
export default class RecoveryPhrase extends Component {
|
export default class RecoveryPhrase extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired
|
store: PropTypes.object.isRequired
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
accountName: '',
|
|
||||||
accountNameError: ERRORS.noName,
|
|
||||||
isValidPass: true,
|
|
||||||
isValidName: false,
|
|
||||||
isValidPhrase: true,
|
|
||||||
passwordHint: '',
|
|
||||||
password1: '',
|
|
||||||
password1Error: null,
|
|
||||||
password2: '',
|
|
||||||
password2Error: null,
|
|
||||||
recoveryPhrase: '',
|
|
||||||
recoveryPhraseError: null,
|
|
||||||
windowsPhrase: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount () {
|
|
||||||
this.props.onChange(false, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error, recoveryPhrase, windowsPhrase } = this.state;
|
const { isWindowsPhrase, name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, phrase } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Input
|
<Input
|
||||||
hint='the account recovery phrase'
|
hint={
|
||||||
label='account recovery phrase'
|
<FormattedMessage
|
||||||
value={ recoveryPhrase }
|
id='createAccount.recoveryPhrase.phrase.hint'
|
||||||
|
defaultMessage='the account recovery phrase'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.phrase.label'
|
||||||
|
defaultMessage='account recovery phrase'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditPhrase }
|
onChange={ this.onEditPhrase }
|
||||||
|
value={ phrase }
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='account name'
|
error={ nameError }
|
||||||
hint='a descriptive name for the account'
|
hint={
|
||||||
error={ accountNameError }
|
<FormattedMessage
|
||||||
value={ accountName }
|
id='createAccount.recoveryPhrase.name.hint'
|
||||||
onChange={ this.onEditAccountName }
|
defaultMessage='a descriptive name for the account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.name.label'
|
||||||
|
defaultMessage='account name'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditName }
|
||||||
|
value={ name }
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='password hint'
|
hint={
|
||||||
hint='(optional) a hint to help with remembering the password'
|
<FormattedMessage
|
||||||
value={ passwordHint }
|
id='createAccount.recoveryPhrase.hint.hint'
|
||||||
|
defaultMessage='(optional) a hint to help with remembering the password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.hint.label'
|
||||||
|
defaultMessage='password hint'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ this.onEditPasswordHint }
|
onChange={ this.onEditPasswordHint }
|
||||||
|
value={ passwordHint }
|
||||||
/>
|
/>
|
||||||
<div className={ styles.passwords }>
|
<div className={ styles.passwords }>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password'
|
hint={
|
||||||
hint='a strong, unique password'
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.password.hint'
|
||||||
|
defaultMessage='a strong, unique password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.password.label'
|
||||||
|
defaultMessage='password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditPassword }
|
||||||
type='password'
|
type='password'
|
||||||
error={ password1Error }
|
value={ password }
|
||||||
value={ password1 }
|
|
||||||
onChange={ this.onEditPassword1 }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.password }>
|
<div className={ styles.password }>
|
||||||
<Input
|
<Input
|
||||||
label='password (repeat)'
|
error={ passwordRepeatError }
|
||||||
hint='verify your password'
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.password2.hint'
|
||||||
|
defaultMessage='verify your password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.password2.label'
|
||||||
|
defaultMessage='password (repeat)'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ this.onEditPasswordRepeat }
|
||||||
type='password'
|
type='password'
|
||||||
error={ password2Error }
|
value={ passwordRepeat }
|
||||||
value={ password2 }
|
|
||||||
onChange={ this.onEditPassword2 }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PasswordStrength input={ password } />
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
checked={ isWindowsPhrase }
|
||||||
className={ styles.checkbox }
|
className={ styles.checkbox }
|
||||||
label='Key was created with Parity <1.4.5 on Windows'
|
label={
|
||||||
checked={ windowsPhrase }
|
<FormattedMessage
|
||||||
|
id='createAccount.recoveryPhrase.windowsKey.label'
|
||||||
|
defaultMessage='Key was created with Parity <1.4.5 on Windows'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onCheck={ this.onToggleWindowsPhrase }
|
onCheck={ this.onToggleWindowsPhrase }
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParent = () => {
|
|
||||||
const { accountName, isValidName, isValidPass, isValidPhrase, password1, passwordHint, recoveryPhrase, windowsPhrase } = this.state;
|
|
||||||
const isValid = isValidName && isValidPass && isValidPhrase;
|
|
||||||
|
|
||||||
this.props.onChange(isValid, {
|
|
||||||
name: accountName,
|
|
||||||
password: password1,
|
|
||||||
passwordHint,
|
|
||||||
phrase: recoveryPhrase,
|
|
||||||
windowsPhrase
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPasswordHint = (event, value) => {
|
|
||||||
this.setState({
|
|
||||||
passwordHint: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleWindowsPhrase = (event) => {
|
onToggleWindowsPhrase = (event) => {
|
||||||
this.setState({
|
const { store } = this.props;
|
||||||
windowsPhrase: !this.state.windowsPhrase
|
|
||||||
}, this.updateParent);
|
store.setWindowsPhrase(!store.isWindowsPhrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditPhrase = (event) => {
|
onEditPhrase = (event, phrase) => {
|
||||||
const recoveryPhrase = event.target.value
|
const { store } = this.props;
|
||||||
.toLowerCase() // wordlists are lowercase
|
|
||||||
.trim() // remove whitespace at both ends
|
|
||||||
.replace(/\s/g, ' ') // replace any whitespace with single space
|
|
||||||
.replace(/ +/g, ' '); // replace multiple spaces with a single space
|
|
||||||
|
|
||||||
const phraseParts = recoveryPhrase
|
store.setPhrase(phrase);
|
||||||
.split(' ')
|
|
||||||
.map((part) => part.trim())
|
|
||||||
.filter((part) => part.length);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
recoveryPhrase: phraseParts.join(' '),
|
|
||||||
recoveryPhraseError: null,
|
|
||||||
isValidPhrase: true
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditAccountName = (event) => {
|
onEditName = (event, name) => {
|
||||||
const accountName = event.target.value;
|
const { store } = this.props;
|
||||||
let accountNameError = null;
|
|
||||||
|
|
||||||
if (!accountName || !accountName.trim().length) {
|
store.setName(name);
|
||||||
accountNameError = ERRORS.noName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
onEditPassword = (event, password) => {
|
||||||
accountName,
|
const { store } = this.props;
|
||||||
accountNameError,
|
|
||||||
isValidName: !accountNameError
|
store.setPassword(password);
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditPassword1 = (event) => {
|
onEditPasswordRepeat = (event, password) => {
|
||||||
const password1 = event.target.value;
|
const { store } = this.props;
|
||||||
let password2Error = null;
|
|
||||||
|
|
||||||
if (password1 !== this.state.password2) {
|
store.setPasswordRepeat(password);
|
||||||
password2Error = ERRORS.noMatchPassword;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
onEditPasswordHint = (event, passwordHint) => {
|
||||||
password1,
|
const { store } = this.props;
|
||||||
password1Error: null,
|
|
||||||
password2Error,
|
|
||||||
isValidPass: !password2Error
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPassword2 = (event) => {
|
store.setPasswordHint(passwordHint);
|
||||||
const password2 = event.target.value;
|
|
||||||
let password2Error = null;
|
|
||||||
|
|
||||||
if (password2 !== this.state.password1) {
|
|
||||||
password2Error = ERRORS.noMatchPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
password2,
|
|
||||||
password2Error,
|
|
||||||
isValidPass: !password2Error
|
|
||||||
}, this.updateParent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { createStore } from '../createAccount.test.js';
|
||||||
|
|
||||||
|
import RecoveryPhrase from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = createStore();
|
||||||
|
component = shallow(
|
||||||
|
<RecoveryPhrase
|
||||||
|
store={ store }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/RecoveryPhrase', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event handlers', () => {
|
||||||
|
describe('onEditName', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setName');
|
||||||
|
instance.onEditName(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setName.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setName).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPhrase', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPhrase');
|
||||||
|
instance.onEditPhrase(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPhrase.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPhrase).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPassword');
|
||||||
|
instance.onEditPassword(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPassword.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPassword).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordRepeat', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordRepeat');
|
||||||
|
instance.onEditPasswordRepeat(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordRepeat.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordRepeat).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onEditPasswordHint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setPasswordHint');
|
||||||
|
instance.onEditPasswordHint(null, 'testValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setPasswordHint.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setPasswordHint).to.have.been.calledWith('testValue');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onToggleWindowsPhrase', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.spy(store, 'setWindowsPhrase');
|
||||||
|
instance.onToggleWindowsPhrase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setWindowsPhrase.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the store', () => {
|
||||||
|
expect(store.setWindowsPhrase).to.have.been.calledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,17 +14,17 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import ActionDone from 'material-ui/svg-icons/action/done';
|
import { createIdentityImg } from '~/api/util/identity';
|
||||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
import { newError } from '~/redux/actions';
|
||||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
import { Button, Modal } from '~/ui';
|
||||||
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
|
import { CancelIcon, CheckIcon, DoneIcon, NextIcon, PrevIcon, PrintIcon } from '~/ui/Icons';
|
||||||
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
|
import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg';
|
||||||
import PrintIcon from 'material-ui/svg-icons/action/print';
|
|
||||||
|
|
||||||
import { Button, Modal, Warning } from '~/ui';
|
|
||||||
|
|
||||||
import AccountDetails from './AccountDetails';
|
import AccountDetails from './AccountDetails';
|
||||||
import AccountDetailsGeth from './AccountDetailsGeth';
|
import AccountDetailsGeth from './AccountDetailsGeth';
|
||||||
@ -34,405 +34,271 @@ import NewGeth from './NewGeth';
|
|||||||
import NewImport from './NewImport';
|
import NewImport from './NewImport';
|
||||||
import RawKey from './RawKey';
|
import RawKey from './RawKey';
|
||||||
import RecoveryPhrase from './RecoveryPhrase';
|
import RecoveryPhrase from './RecoveryPhrase';
|
||||||
|
import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store';
|
||||||
import { createIdentityImg } from '~/api/util/identity';
|
|
||||||
import print from './print';
|
import print from './print';
|
||||||
import recoveryPage from './recovery-page.ejs';
|
import recoveryPage from './recoveryPage.ejs';
|
||||||
import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg';
|
|
||||||
|
|
||||||
const TITLES = {
|
const TITLES = {
|
||||||
type: 'creation type',
|
type: (
|
||||||
create: 'create account',
|
<FormattedMessage
|
||||||
info: 'account information',
|
id='createAccount.title.createType'
|
||||||
import: 'import wallet'
|
defaultMessage='creation type'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
create: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.title.createAccount'
|
||||||
|
defaultMessage='create account'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
info: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.title.accountInfo'
|
||||||
|
defaultMessage='account information'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
import: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.title.importWallet'
|
||||||
|
defaultMessage='import wallet'
|
||||||
|
/>
|
||||||
|
)
|
||||||
};
|
};
|
||||||
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info];
|
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info];
|
||||||
const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info];
|
const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info];
|
||||||
|
|
||||||
export default class CreateAccount extends Component {
|
@observer
|
||||||
|
class CreateAccount extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired,
|
api: PropTypes.object.isRequired
|
||||||
store: PropTypes.object.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accounts: PropTypes.object.isRequired,
|
accounts: PropTypes.object.isRequired,
|
||||||
|
newError: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
onUpdate: PropTypes.func
|
onUpdate: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = new Store(this.context.api, this.props.accounts);
|
||||||
address: null,
|
|
||||||
name: null,
|
|
||||||
passwordHint: null,
|
|
||||||
password: null,
|
|
||||||
phrase: null,
|
|
||||||
windowsPhrase: false,
|
|
||||||
rawKey: null,
|
|
||||||
json: null,
|
|
||||||
canCreate: false,
|
|
||||||
createType: null,
|
|
||||||
gethAddresses: [],
|
|
||||||
stage: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { createType, stage } = this.state;
|
const { createType, stage } = this.store;
|
||||||
const steps = createType === 'fromNew'
|
|
||||||
? STAGE_NAMES
|
|
||||||
: STAGE_IMPORT;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
actions={ this.renderDialogActions() }
|
actions={ this.renderDialogActions() }
|
||||||
current={ stage }
|
current={ stage }
|
||||||
steps={ steps }
|
steps={
|
||||||
|
createType === 'fromNew'
|
||||||
|
? STAGE_NAMES
|
||||||
|
: STAGE_IMPORT
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{ this.renderWarning() }
|
|
||||||
{ this.renderPage() }
|
{ this.renderPage() }
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPage () {
|
renderPage () {
|
||||||
const { createType, stage } = this.state;
|
const { createType, stage } = this.store;
|
||||||
const { accounts } = this.props;
|
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case STAGE_SELECT_TYPE:
|
||||||
return (
|
return (
|
||||||
<CreationType onChange={ this.onChangeType } />
|
<CreationType store={ this.store } />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 1:
|
case STAGE_CREATE:
|
||||||
if (createType === 'fromNew') {
|
if (createType === 'fromNew') {
|
||||||
return (
|
return (
|
||||||
<NewAccount onChange={ this.onChangeDetails } />
|
<NewAccount
|
||||||
|
newError={ this.props.newError }
|
||||||
|
store={ this.store }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createType === 'fromGeth') {
|
if (createType === 'fromGeth') {
|
||||||
return (
|
return (
|
||||||
<NewGeth
|
<NewGeth store={ this.store } />
|
||||||
accounts={ accounts }
|
|
||||||
onChange={ this.onChangeGeth }
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createType === 'fromPhrase') {
|
if (createType === 'fromPhrase') {
|
||||||
return (
|
return (
|
||||||
<RecoveryPhrase onChange={ this.onChangeDetails } />
|
<RecoveryPhrase store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createType === 'fromRaw') {
|
if (createType === 'fromRaw') {
|
||||||
return (
|
return (
|
||||||
<RawKey onChange={ this.onChangeDetails } />
|
<RawKey store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewImport onChange={ this.onChangeWallet } />
|
<NewImport store={ this.store } />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 2:
|
case STAGE_INFO:
|
||||||
if (createType === 'fromGeth') {
|
if (createType === 'fromGeth') {
|
||||||
return (
|
return (
|
||||||
<AccountDetailsGeth addresses={ this.state.gethAddresses } />
|
<AccountDetailsGeth store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountDetails
|
<AccountDetails store={ this.store } />
|
||||||
address={ this.state.address }
|
|
||||||
name={ this.state.name }
|
|
||||||
phrase={ this.state.phrase }
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDialogActions () {
|
renderDialogActions () {
|
||||||
const { createType, stage } = this.state;
|
const { createType, canCreate, isBusy, stage } = this.store;
|
||||||
|
|
||||||
|
const cancelBtn = (
|
||||||
|
<Button
|
||||||
|
icon={ <CancelIcon /> }
|
||||||
|
key='cancel'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.button.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={ this.onClose }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case STAGE_SELECT_TYPE:
|
||||||
return [
|
return [
|
||||||
|
cancelBtn,
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentClear /> }
|
icon={ <NextIcon /> }
|
||||||
label='Cancel'
|
key='next'
|
||||||
onClick={ this.onClose }
|
label={
|
||||||
/>,
|
<FormattedMessage
|
||||||
<Button
|
id='createAccount.button.next'
|
||||||
icon={ <NavigationArrowForward /> }
|
defaultMessage='Next'
|
||||||
label='Next'
|
/>
|
||||||
onClick={ this.onNext }
|
}
|
||||||
|
onClick={ this.store.nextStage }
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
case 1:
|
|
||||||
const createLabel = createType === 'fromNew'
|
|
||||||
? 'Create'
|
|
||||||
: 'Import';
|
|
||||||
|
|
||||||
|
case STAGE_CREATE:
|
||||||
return [
|
return [
|
||||||
|
cancelBtn,
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentClear /> }
|
icon={ <PrevIcon /> }
|
||||||
label='Cancel'
|
key='back'
|
||||||
onClick={ this.onClose }
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.button.back'
|
||||||
|
defaultMessage='Back'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={ this.store.prevStage }
|
||||||
/>,
|
/>,
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowBack /> }
|
disabled={ !canCreate || isBusy }
|
||||||
label='Back'
|
icon={ <CheckIcon /> }
|
||||||
onClick={ this.onPrev }
|
key='create'
|
||||||
/>,
|
label={
|
||||||
<Button
|
createType === 'fromNew'
|
||||||
icon={ <ActionDone /> }
|
? (
|
||||||
label={ createLabel }
|
<FormattedMessage
|
||||||
disabled={ !this.state.canCreate }
|
id='createAccount.button.create'
|
||||||
|
defaultMessage='Create'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.button.import'
|
||||||
|
defaultMessage='Import'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={ this.onCreate }
|
onClick={ this.onCreate }
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
|
|
||||||
case 2:
|
case STAGE_INFO:
|
||||||
return [
|
return [
|
||||||
createType === 'fromNew' || createType === 'fromPhrase' ? (
|
['fromNew', 'fromPhrase'].includes(createType)
|
||||||
|
? (
|
||||||
<Button
|
<Button
|
||||||
icon={ <PrintIcon /> }
|
icon={ <PrintIcon /> }
|
||||||
label='Print Phrase'
|
key='print'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.button.print'
|
||||||
|
defaultMessage='Print Phrase'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={ this.printPhrase }
|
onClick={ this.printPhrase }
|
||||||
/>
|
/>
|
||||||
) : null,
|
)
|
||||||
|
: null,
|
||||||
<Button
|
<Button
|
||||||
icon={ <ActionDoneAll /> }
|
icon={ <DoneIcon /> }
|
||||||
label='Close'
|
key='close'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='createAccount.button.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={ this.onClose }
|
onClick={ this.onClose }
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWarning () {
|
|
||||||
const { createType, stage } = this.state;
|
|
||||||
|
|
||||||
if (stage !== 1 || ['fromJSON', 'fromPresale'].includes(createType)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Warning
|
|
||||||
warning={
|
|
||||||
<FormattedMessage
|
|
||||||
id='createAccount.warning.insecurePassword'
|
|
||||||
defaultMessage='It is recommended that a strong password be used to secure your accounts. Empty and trivial passwords are a security risk.'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onNext = () => {
|
|
||||||
this.setState({
|
|
||||||
stage: this.state.stage + 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPrev = () => {
|
|
||||||
this.setState({
|
|
||||||
stage: this.state.stage - 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreate = () => {
|
onCreate = () => {
|
||||||
const { createType, windowsPhrase } = this.state;
|
this.store.setBusy(true);
|
||||||
const { api } = this.context;
|
|
||||||
|
|
||||||
this.setState({
|
return this.store
|
||||||
canCreate: false
|
.createAccount()
|
||||||
});
|
|
||||||
|
|
||||||
if (createType === 'fromNew' || createType === 'fromPhrase') {
|
|
||||||
let phrase = this.state.phrase;
|
|
||||||
|
|
||||||
if (createType === 'fromPhrase' && windowsPhrase) {
|
|
||||||
phrase = phrase
|
|
||||||
.split(' ') // get the words
|
|
||||||
.map((word) => word === 'misjudged' ? word : `${word}\r`) // add \r after each (except last in dict)
|
|
||||||
.join(' '); // re-create string
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.parity
|
|
||||||
.newAccountFromPhrase(phrase, this.state.password)
|
|
||||||
.then((address) => {
|
|
||||||
this.setState({ address });
|
|
||||||
return api.parity
|
|
||||||
.setAccountName(address, this.state.name)
|
|
||||||
.then(() => api.parity.setAccountMeta(address, {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
passwordHint: this.state.passwordHint
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.onNext();
|
this.store.setBusy(false);
|
||||||
|
this.store.nextStage();
|
||||||
this.props.onUpdate && this.props.onUpdate();
|
this.props.onUpdate && this.props.onUpdate();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('onCreate', error);
|
this.store.setBusy(false);
|
||||||
|
this.props.newError(error);
|
||||||
this.setState({
|
|
||||||
canCreate: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createType === 'fromRaw') {
|
|
||||||
return api.parity
|
|
||||||
.newAccountFromSecret(this.state.rawKey, this.state.password)
|
|
||||||
.then((address) => {
|
|
||||||
this.setState({ address });
|
|
||||||
return api.parity
|
|
||||||
.setAccountName(address, this.state.name)
|
|
||||||
.then(() => api.parity.setAccountMeta(address, {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
passwordHint: this.state.passwordHint
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.onNext();
|
|
||||||
this.props.onUpdate && this.props.onUpdate();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('onCreate', error);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
canCreate: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createType === 'fromGeth') {
|
|
||||||
return api.parity
|
|
||||||
.importGethAccounts(this.state.gethAddresses)
|
|
||||||
.then((result) => {
|
|
||||||
console.log('result', result);
|
|
||||||
|
|
||||||
return Promise.all(this.state.gethAddresses.map((address) => {
|
|
||||||
return api.parity.setAccountName(address, 'Geth Import');
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.onNext();
|
|
||||||
this.props.onUpdate && this.props.onUpdate();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('onCreate', error);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
canCreate: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.parity
|
|
||||||
.newAccountFromWallet(this.state.json, this.state.password)
|
|
||||||
.then((address) => {
|
|
||||||
this.setState({
|
|
||||||
address: address
|
|
||||||
});
|
|
||||||
|
|
||||||
return api.parity
|
|
||||||
.setAccountName(address, this.state.name)
|
|
||||||
.then(() => api.parity.setAccountMeta(address, {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
passwordHint: this.state.passwordHint
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.onNext();
|
|
||||||
this.props.onUpdate && this.props.onUpdate();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('onCreate', error);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
canCreate: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.setState({
|
|
||||||
stage: 0,
|
|
||||||
canCreate: false
|
|
||||||
}, () => {
|
|
||||||
this.props.onClose && this.props.onClose();
|
this.props.onClose && this.props.onClose();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeType = (value) => {
|
|
||||||
this.setState({
|
|
||||||
createType: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => {
|
|
||||||
const nextState = {
|
|
||||||
canCreate,
|
|
||||||
name,
|
|
||||||
passwordHint,
|
|
||||||
address,
|
|
||||||
password,
|
|
||||||
phrase,
|
|
||||||
windowsPhrase: windowsPhrase || false,
|
|
||||||
rawKey
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeRaw = (canCreate, rawKey) => {
|
|
||||||
this.setState({
|
|
||||||
canCreate,
|
|
||||||
rawKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeGeth = (canCreate, gethAddresses) => {
|
|
||||||
this.setState({
|
|
||||||
canCreate,
|
|
||||||
gethAddresses
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeWallet = (canCreate, { name, passwordHint, password, json }) => {
|
|
||||||
this.setState({
|
|
||||||
canCreate,
|
|
||||||
name,
|
|
||||||
passwordHint,
|
|
||||||
password,
|
|
||||||
json
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
newError = (error) => {
|
|
||||||
const { store } = this.context;
|
|
||||||
|
|
||||||
store.dispatch({ type: 'newError', error });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printPhrase = () => {
|
printPhrase = () => {
|
||||||
const { address, phrase, name } = this.state;
|
const { address, name, phrase } = this.store;
|
||||||
const identity = createIdentityImg(address);
|
const identity = createIdentityImg(address);
|
||||||
|
|
||||||
print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo }));
|
print(recoveryPage({
|
||||||
|
address,
|
||||||
|
identity,
|
||||||
|
logo: ParityLogo,
|
||||||
|
name,
|
||||||
|
phrase
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps (dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
newError
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CreateAccount);
|
||||||
|
51
js/src/modals/CreateAccount/createAccount.spec.js
Normal file
51
js/src/modals/CreateAccount/createAccount.spec.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ACCOUNTS, createApi, createRedux } from './createAccount.test.js';
|
||||||
|
|
||||||
|
import CreateAccount from './';
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
api = createApi();
|
||||||
|
component = shallow(
|
||||||
|
<CreateAccount
|
||||||
|
accounts={ ACCOUNTS }
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
store: createRedux()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).find('CreateAccount').shallow({
|
||||||
|
context: { api }
|
||||||
|
});
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('renders with defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
71
js/src/modals/CreateAccount/createAccount.test.js
Normal file
71
js/src/modals/CreateAccount/createAccount.test.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Store from './store';
|
||||||
|
|
||||||
|
const ADDRESS = '0x00000123456789abcdef123456789abcdef123456789abcdef';
|
||||||
|
const ACCOUNTS = { [ADDRESS]: {} };
|
||||||
|
const GETH_ADDRESSES = [
|
||||||
|
'0x123456789abcdef123456789abcdef123456789abcdef00000',
|
||||||
|
'0x00000123456789abcdef123456789abcdef123456789abcdef'
|
||||||
|
];
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
function createApi () {
|
||||||
|
return {
|
||||||
|
eth: {
|
||||||
|
getBalance: sinon.stub().resolves(new BigNumber(1))
|
||||||
|
},
|
||||||
|
parity: {
|
||||||
|
generateSecretPhrase: sinon.stub().resolves('some account phrase'),
|
||||||
|
importGethAccounts: sinon.stub().resolves(),
|
||||||
|
listGethAccounts: sinon.stub().resolves(GETH_ADDRESSES),
|
||||||
|
newAccountFromPhrase: sinon.stub().resolves(ADDRESS),
|
||||||
|
newAccountFromSecret: sinon.stub().resolves(ADDRESS),
|
||||||
|
newAccountFromWallet: sinon.stub().resolves(ADDRESS),
|
||||||
|
phraseToAddress: () => Promise.resolve(`${++counter}`),
|
||||||
|
setAccountMeta: sinon.stub().resolves(),
|
||||||
|
setAccountName: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRedux () {
|
||||||
|
return {
|
||||||
|
dispatch: sinon.stub(),
|
||||||
|
subscribe: sinon.stub(),
|
||||||
|
getState: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStore () {
|
||||||
|
return new Store(createApi(), ACCOUNTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ACCOUNTS,
|
||||||
|
ADDRESS,
|
||||||
|
GETH_ADDRESSES,
|
||||||
|
createApi,
|
||||||
|
createRedux,
|
||||||
|
createStore
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Recovery phrase for <%= name %></title>
|
<title>Recovery phrase for <%= name %></title>
|
379
js/src/modals/CreateAccount/store.js
Normal file
379
js/src/modals/CreateAccount/store.js
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { action, computed, observable, transaction } from 'mobx';
|
||||||
|
|
||||||
|
import apiutil from '~/api/util';
|
||||||
|
|
||||||
|
import ERRORS from './errors';
|
||||||
|
|
||||||
|
const FAKEPATH = 'C:\\fakepath\\';
|
||||||
|
const STAGE_SELECT_TYPE = 0;
|
||||||
|
const STAGE_CREATE = 1;
|
||||||
|
const STAGE_INFO = 2;
|
||||||
|
|
||||||
|
export default class Store {
|
||||||
|
@observable accounts = null;
|
||||||
|
@observable address = null;
|
||||||
|
@observable createType = 'fromNew';
|
||||||
|
@observable description = '';
|
||||||
|
@observable gethAccountsAvailable = [];
|
||||||
|
@observable gethAddresses = [];
|
||||||
|
@observable isBusy = false;
|
||||||
|
@observable isWindowsPhrase = false;
|
||||||
|
@observable name = '';
|
||||||
|
@observable nameError = ERRORS.noName;
|
||||||
|
@observable password = '';
|
||||||
|
@observable passwordHint = '';
|
||||||
|
@observable passwordRepeat = '';
|
||||||
|
@observable phrase = '';
|
||||||
|
@observable rawKey = '';
|
||||||
|
@observable rawKeyError = ERRORS.nokey;
|
||||||
|
@observable stage = STAGE_SELECT_TYPE;
|
||||||
|
@observable walletFile = '';
|
||||||
|
@observable walletFileError = ERRORS.noFile;
|
||||||
|
@observable walletJson = '';
|
||||||
|
|
||||||
|
constructor (api, accounts, loadGeth = true) {
|
||||||
|
this._api = api;
|
||||||
|
this.accounts = Object.assign({}, accounts);
|
||||||
|
|
||||||
|
if (loadGeth) {
|
||||||
|
this.loadAvailableGethAccounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get canCreate () {
|
||||||
|
switch (this.createType) {
|
||||||
|
case 'fromGeth':
|
||||||
|
return this.gethAddresses.length !== 0;
|
||||||
|
|
||||||
|
case 'fromJSON':
|
||||||
|
case 'fromPresale':
|
||||||
|
return !(this.nameError || this.walletFileError);
|
||||||
|
|
||||||
|
case 'fromNew':
|
||||||
|
return !(this.nameError || this.passwordRepeatError);
|
||||||
|
|
||||||
|
case 'fromPhrase':
|
||||||
|
return !(this.nameError || this.passwordRepeatError);
|
||||||
|
|
||||||
|
case 'fromRaw':
|
||||||
|
return !(this.nameError || this.passwordRepeatError || this.rawKeyError);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get passwordRepeatError () {
|
||||||
|
return this.password === this.passwordRepeat
|
||||||
|
? null
|
||||||
|
: ERRORS.noMatchPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action clearErrors = () => {
|
||||||
|
transaction(() => {
|
||||||
|
this.password = '';
|
||||||
|
this.passwordRepeat = '';
|
||||||
|
this.nameError = null;
|
||||||
|
this.rawKeyError = null;
|
||||||
|
this.walletFileError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action selectGethAccount = (address) => {
|
||||||
|
if (this.gethAddresses.includes(address)) {
|
||||||
|
this.gethAddresses = this.gethAddresses.filter((_address) => _address !== address);
|
||||||
|
} else {
|
||||||
|
this.gethAddresses = [address].concat(this.gethAddresses.peek());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setAddress = (address) => {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setBusy = (isBusy) => {
|
||||||
|
this.isBusy = isBusy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setCreateType = (createType) => {
|
||||||
|
this.clearErrors();
|
||||||
|
this.createType = createType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setDescription = (description) => {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setGethAccountsAvailable = (gethAccountsAvailable) => {
|
||||||
|
this.gethAccountsAvailable = [].concat(gethAccountsAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setWindowsPhrase = (isWindowsPhrase = false) => {
|
||||||
|
this.isWindowsPhrase = isWindowsPhrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setName = (name) => {
|
||||||
|
let nameError = null;
|
||||||
|
|
||||||
|
if (!name || !name.trim().length) {
|
||||||
|
nameError = ERRORS.noName;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.name = name;
|
||||||
|
this.nameError = nameError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setPassword = (password) => {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setPasswordHint = (passwordHint) => {
|
||||||
|
this.passwordHint = passwordHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setPasswordRepeat = (passwordRepeat) => {
|
||||||
|
this.passwordRepeat = passwordRepeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setPhrase = (phrase) => {
|
||||||
|
const recoveryPhrase = phrase
|
||||||
|
.toLowerCase() // wordlists are lowercase
|
||||||
|
.trim() // remove whitespace at both ends
|
||||||
|
.replace(/\s/g, ' ') // replace any whitespace with single space
|
||||||
|
.replace(/ +/g, ' '); // replace multiple spaces with a single space
|
||||||
|
|
||||||
|
const phraseParts = recoveryPhrase
|
||||||
|
.split(' ')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter((part) => part.length);
|
||||||
|
|
||||||
|
this.phrase = phraseParts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setRawKey = (rawKey) => {
|
||||||
|
let rawKeyError = null;
|
||||||
|
|
||||||
|
if (!rawKey || !rawKey.trim().length) {
|
||||||
|
rawKeyError = ERRORS.noKey;
|
||||||
|
} else if (rawKey.substr(0, 2) !== '0x' || rawKey.substr(2).length !== 64 || !apiutil.isHex(rawKey)) {
|
||||||
|
rawKeyError = ERRORS.invalidKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.rawKey = rawKey;
|
||||||
|
this.rawKeyError = rawKeyError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setStage = (stage) => {
|
||||||
|
this.stage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setWalletFile = (walletFile) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.walletFile = walletFile.replace(FAKEPATH, '');
|
||||||
|
this.walletFileError = ERRORS.noFile;
|
||||||
|
this.walletJson = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setWalletJson = (walletJson) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.walletFileError = null;
|
||||||
|
this.walletJson = walletJson;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action nextStage = () => {
|
||||||
|
this.stage++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action prevStage = () => {
|
||||||
|
this.stage--;
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccount = () => {
|
||||||
|
switch (this.createType) {
|
||||||
|
case 'fromGeth':
|
||||||
|
return this.createAccountFromGeth();
|
||||||
|
|
||||||
|
case 'fromJSON':
|
||||||
|
case 'fromPresale':
|
||||||
|
return this.createAccountFromWallet();
|
||||||
|
|
||||||
|
case 'fromNew':
|
||||||
|
case 'fromPhrase':
|
||||||
|
return this.createAccountFromPhrase();
|
||||||
|
|
||||||
|
case 'fromRaw':
|
||||||
|
return this.createAccountFromRaw();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Cannot create account for ${this.createType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccountFromGeth = (timestamp = Date.now()) => {
|
||||||
|
return this._api.parity
|
||||||
|
.importGethAccounts(this.gethAddresses.peek())
|
||||||
|
.then(() => {
|
||||||
|
return Promise.all(this.gethAddresses.map((address) => {
|
||||||
|
return this._api.parity.setAccountName(address, 'Geth Import');
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return Promise.all(this.gethAddresses.map((address) => {
|
||||||
|
return this._api.parity.setAccountMeta(address, {
|
||||||
|
timestamp
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('createAccount', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccountFromPhrase = (timestamp = Date.now()) => {
|
||||||
|
let formattedPhrase = this.phrase;
|
||||||
|
|
||||||
|
if (this.isWindowsPhrase && this.createType === 'fromPhrase') {
|
||||||
|
formattedPhrase = this.phrase
|
||||||
|
.split(' ') // get the words
|
||||||
|
.map((word) => word === 'misjudged' ? word : `${word}\r`) // add \r after each (except last in dict)
|
||||||
|
.join(' '); // re-create string
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._api.parity
|
||||||
|
.newAccountFromPhrase(formattedPhrase, this.password)
|
||||||
|
.then((address) => {
|
||||||
|
this.setAddress(address);
|
||||||
|
|
||||||
|
return this._api.parity
|
||||||
|
.setAccountName(address, this.name)
|
||||||
|
.then(() => this._api.parity.setAccountMeta(address, {
|
||||||
|
passwordHint: this.passwordHint,
|
||||||
|
timestamp
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('createAccount', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccountFromRaw = (timestamp = Date.now()) => {
|
||||||
|
return this._api.parity
|
||||||
|
.newAccountFromSecret(this.rawKey, this.password)
|
||||||
|
.then((address) => {
|
||||||
|
this.setAddress(address);
|
||||||
|
|
||||||
|
return this._api.parity
|
||||||
|
.setAccountName(address, this.name)
|
||||||
|
.then(() => this._api.parity.setAccountMeta(address, {
|
||||||
|
passwordHint: this.passwordHint,
|
||||||
|
timestamp
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('createAccount', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccountFromWallet = (timestamp = Date.now()) => {
|
||||||
|
return this._api.parity
|
||||||
|
.newAccountFromWallet(this.walletJson, this.password)
|
||||||
|
.then((address) => {
|
||||||
|
this.setAddress(address);
|
||||||
|
|
||||||
|
return this._api.parity
|
||||||
|
.setAccountName(address, this.name)
|
||||||
|
.then(() => this._api.parity.setAccountMeta(address, {
|
||||||
|
passwordHint: this.passwordHint,
|
||||||
|
timestamp
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('createAccount', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createIdentities = () => {
|
||||||
|
return Promise
|
||||||
|
.all([
|
||||||
|
this._api.parity.generateSecretPhrase(),
|
||||||
|
this._api.parity.generateSecretPhrase(),
|
||||||
|
this._api.parity.generateSecretPhrase(),
|
||||||
|
this._api.parity.generateSecretPhrase(),
|
||||||
|
this._api.parity.generateSecretPhrase()
|
||||||
|
])
|
||||||
|
.then((phrases) => {
|
||||||
|
return Promise
|
||||||
|
.all(phrases.map((phrase) => this._api.parity.phraseToAddress(phrase)))
|
||||||
|
.then((addresses) => {
|
||||||
|
return phrases.reduce((accounts, phrase, index) => {
|
||||||
|
const address = addresses[index];
|
||||||
|
|
||||||
|
accounts[address] = {
|
||||||
|
address,
|
||||||
|
phrase
|
||||||
|
};
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('createIdentities', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAvailableGethAccounts () {
|
||||||
|
return this._api.parity
|
||||||
|
.listGethAccounts()
|
||||||
|
.then((_addresses) => {
|
||||||
|
const addresses = (_addresses || []).filter((address) => !this.accounts[address]);
|
||||||
|
|
||||||
|
return Promise
|
||||||
|
.all(addresses.map((address) => this._api.eth.getBalance(address)))
|
||||||
|
.then((balances) => {
|
||||||
|
this.setGethAccountsAvailable(addresses.map((address, index) => {
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
balance: apiutil.fromWei(balances[index]).toFormat(5)
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('loadAvailableGethAccounts', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
STAGE_CREATE,
|
||||||
|
STAGE_INFO,
|
||||||
|
STAGE_SELECT_TYPE
|
||||||
|
};
|
624
js/src/modals/CreateAccount/store.spec.js
Normal file
624
js/src/modals/CreateAccount/store.spec.js
Normal file
@ -0,0 +1,624 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Store from './store';
|
||||||
|
|
||||||
|
import { ACCOUNTS, ADDRESS, GETH_ADDRESSES, createApi } from './createAccount.test.js';
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function createStore (loadGeth) {
|
||||||
|
api = createApi();
|
||||||
|
store = new Store(api, ACCOUNTS, loadGeth);
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/CreateAccount/Store', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('captures the accounts passed', () => {
|
||||||
|
expect(store.accounts).to.deep.equal(ACCOUNTS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts as non-busy', () => {
|
||||||
|
expect(store.isBusy).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the initial createType to fromNew', () => {
|
||||||
|
expect(store.createType).to.equal('fromNew');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the initial stage to create', () => {
|
||||||
|
expect(store.stage).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the geth accounts', () => {
|
||||||
|
expect(store.gethAccountsAvailable.map((account) => account.address)).to.deep.equal([GETH_ADDRESSES[0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not load geth accounts when loadGeth === false', () => {
|
||||||
|
createStore(false);
|
||||||
|
expect(store.gethAccountsAvailable.peek()).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@action', () => {
|
||||||
|
describe('clearErrors', () => {
|
||||||
|
it('clears all errors', () => {
|
||||||
|
store.clearErrors();
|
||||||
|
|
||||||
|
expect(store.nameError).to.be.null;
|
||||||
|
expect(store.passwordRepeatError).to.be.null;
|
||||||
|
expect(store.rawKeyError).to.be.null;
|
||||||
|
expect(store.walletFileError).to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectGethAccount', () => {
|
||||||
|
it('selects and deselects and address', () => {
|
||||||
|
expect(store.gethAddresses.peek()).to.deep.equal([]);
|
||||||
|
store.selectGethAccount(GETH_ADDRESSES[0]);
|
||||||
|
expect(store.gethAddresses.peek()).to.deep.equal([GETH_ADDRESSES[0]]);
|
||||||
|
store.selectGethAccount(GETH_ADDRESSES[0]);
|
||||||
|
expect(store.gethAddresses.peek()).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAddress', () => {
|
||||||
|
const ADDR = '0x1234567890123456789012345678901234567890';
|
||||||
|
|
||||||
|
it('sets the address', () => {
|
||||||
|
store.setAddress(ADDR);
|
||||||
|
expect(store.address).to.equal(ADDR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setBusy', () => {
|
||||||
|
it('sets the busy flag', () => {
|
||||||
|
store.setBusy(true);
|
||||||
|
expect(store.isBusy).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCreateType', () => {
|
||||||
|
it('allows changing the type', () => {
|
||||||
|
store.setCreateType('testing');
|
||||||
|
expect(store.createType).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDescription', () => {
|
||||||
|
it('allows setting the description', () => {
|
||||||
|
store.setDescription('testing');
|
||||||
|
expect(store.description).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setName', () => {
|
||||||
|
it('allows setting the name', () => {
|
||||||
|
store.setName('testing');
|
||||||
|
expect(store.name).to.equal('testing');
|
||||||
|
expect(store.nameError).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets errors on invalid names', () => {
|
||||||
|
store.setName('');
|
||||||
|
expect(store.nameError).not.to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPassword', () => {
|
||||||
|
it('allows setting the password', () => {
|
||||||
|
store.setPassword('testing');
|
||||||
|
expect(store.password).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPasswordHint', () => {
|
||||||
|
it('allows setting the passwordHint', () => {
|
||||||
|
store.setPasswordHint('testing');
|
||||||
|
expect(store.passwordHint).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPasswordRepeat', () => {
|
||||||
|
it('allows setting the passwordRepeat', () => {
|
||||||
|
store.setPasswordRepeat('testing');
|
||||||
|
expect(store.passwordRepeat).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPhrase', () => {
|
||||||
|
it('allows setting the phrase', () => {
|
||||||
|
store.setPhrase('testing');
|
||||||
|
expect(store.phrase).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setRawKey', () => {
|
||||||
|
it('sets error when empty key', () => {
|
||||||
|
store.setRawKey(null);
|
||||||
|
expect(store.rawKeyError).not.to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when non-hex value', () => {
|
||||||
|
store.setRawKey('0000000000000000000000000000000000000000000000000000000000000000');
|
||||||
|
expect(store.rawKeyError).not.to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when non-valid length value', () => {
|
||||||
|
store.setRawKey('0x0');
|
||||||
|
expect(store.rawKeyError).not.to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the key when checks pass', () => {
|
||||||
|
const KEY = '0x1000000000000000000000000000000000000000000000000000000000000000';
|
||||||
|
|
||||||
|
store.setRawKey(KEY);
|
||||||
|
expect(store.rawKey).to.equal(KEY);
|
||||||
|
expect(store.rawKeyError).to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStage', () => {
|
||||||
|
it('changes to the provided stage', () => {
|
||||||
|
store.setStage(2);
|
||||||
|
expect(store.stage).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWalletFile', () => {
|
||||||
|
it('sets the filepath', () => {
|
||||||
|
store.setWalletFile('testing');
|
||||||
|
expect(store.walletFile).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up the fakepath', () => {
|
||||||
|
store.setWalletFile('C:\\fakepath\\testing');
|
||||||
|
expect(store.walletFile).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the error', () => {
|
||||||
|
store.setWalletFile('testing');
|
||||||
|
expect(store.walletFileError).not.to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWalletJson', () => {
|
||||||
|
it('sets the json', () => {
|
||||||
|
store.setWalletJson('testing');
|
||||||
|
expect(store.walletJson).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears previous file errors', () => {
|
||||||
|
store.setWalletFile('testing');
|
||||||
|
store.setWalletJson('testing');
|
||||||
|
expect(store.walletFileError).to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWindowsPhrase', () => {
|
||||||
|
it('allows setting the windows toggle', () => {
|
||||||
|
store.setWindowsPhrase(true);
|
||||||
|
expect(store.isWindowsPhrase).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nextStage/prevStage', () => {
|
||||||
|
it('changes to next/prev', () => {
|
||||||
|
expect(store.stage).to.equal(0);
|
||||||
|
store.nextStage();
|
||||||
|
expect(store.stage).to.equal(1);
|
||||||
|
store.prevStage();
|
||||||
|
expect(store.stage).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@computed', () => {
|
||||||
|
describe('canCreate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.clearErrors();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createType === fromGeth', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('fromGeth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on none selected', () => {
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when selected', () => {
|
||||||
|
store.selectGethAccount(GETH_ADDRESSES[0]);
|
||||||
|
expect(store.canCreate).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createType === fromJSON/fromPresale', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('fromJSON');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true on no errors', () => {
|
||||||
|
expect(store.canCreate).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on nameError', () => {
|
||||||
|
store.setName('');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on walletFileError', () => {
|
||||||
|
store.setWalletFile('testing');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createType === fromNew', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('fromNew');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true on no errors', () => {
|
||||||
|
expect(store.canCreate).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on nameError', () => {
|
||||||
|
store.setName('');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on passwordRepeatError', () => {
|
||||||
|
store.setPassword('testing');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createType === fromPhrase', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('fromPhrase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true on no errors', () => {
|
||||||
|
expect(store.canCreate).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on nameError', () => {
|
||||||
|
store.setName('');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on passwordRepeatError', () => {
|
||||||
|
store.setPassword('testing');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createType === fromRaw', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('fromRaw');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true on no errors', () => {
|
||||||
|
expect(store.canCreate).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on nameError', () => {
|
||||||
|
store.setName('');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on passwordRepeatError', () => {
|
||||||
|
store.setPassword('testing');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on rawKeyError', () => {
|
||||||
|
store.setRawKey('testing');
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createType === anythingElse', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('anythingElse');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always returns false', () => {
|
||||||
|
expect(store.canCreate).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('passwordRepeatError', () => {
|
||||||
|
it('is clear when passwords match', () => {
|
||||||
|
store.setPassword('testing');
|
||||||
|
store.setPasswordRepeat('testing');
|
||||||
|
expect(store.passwordRepeatError).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has error when passwords does not match', () => {
|
||||||
|
store.setPassword('testing');
|
||||||
|
store.setPasswordRepeat('testing2');
|
||||||
|
expect(store.passwordRepeatError).not.to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('operations', () => {
|
||||||
|
describe('createAccount', () => {
|
||||||
|
let createAccountFromGethSpy;
|
||||||
|
let createAccountFromWalletSpy;
|
||||||
|
let createAccountFromPhraseSpy;
|
||||||
|
let createAccountFromRawSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createAccountFromGethSpy = sinon.spy(store, 'createAccountFromGeth');
|
||||||
|
createAccountFromWalletSpy = sinon.spy(store, 'createAccountFromWallet');
|
||||||
|
createAccountFromPhraseSpy = sinon.spy(store, 'createAccountFromPhrase');
|
||||||
|
createAccountFromRawSpy = sinon.spy(store, 'createAccountFromRaw');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error on invalid createType', () => {
|
||||||
|
store.setCreateType('testing');
|
||||||
|
expect(() => store.createAccount()).to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createAccountFromGeth on createType === fromGeth', () => {
|
||||||
|
store.setCreateType('fromGeth');
|
||||||
|
store.createAccount();
|
||||||
|
expect(createAccountFromGethSpy).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createAccountFromWallet on createType === fromJSON', () => {
|
||||||
|
store.setCreateType('fromJSON');
|
||||||
|
store.createAccount();
|
||||||
|
expect(createAccountFromWalletSpy).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createAccountFromPhrase on createType === fromNew', () => {
|
||||||
|
store.setCreateType('fromNew');
|
||||||
|
store.createAccount();
|
||||||
|
expect(createAccountFromPhraseSpy).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createAccountFromPhrase on createType === fromPhrase', () => {
|
||||||
|
store.setCreateType('fromPhrase');
|
||||||
|
store.createAccount();
|
||||||
|
expect(createAccountFromPhraseSpy).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createAccountFromWallet on createType === fromPresale', () => {
|
||||||
|
store.setCreateType('fromPresale');
|
||||||
|
store.createAccount();
|
||||||
|
expect(createAccountFromWalletSpy).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createAccountFromRaw on createType === fromRaw', () => {
|
||||||
|
store.setCreateType('fromRaw');
|
||||||
|
store.createAccount();
|
||||||
|
expect(createAccountFromRawSpy).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAccountFromGeth', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.selectGethAccount(GETH_ADDRESSES[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls parity.importGethAccounts', () => {
|
||||||
|
return store.createAccountFromGeth().then(() => {
|
||||||
|
expect(store._api.parity.importGethAccounts).to.have.been.calledWith([GETH_ADDRESSES[0]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account name', () => {
|
||||||
|
return store.createAccountFromGeth().then(() => {
|
||||||
|
expect(store._api.parity.setAccountName).to.have.been.calledWith(GETH_ADDRESSES[0], 'Geth Import');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account meta', () => {
|
||||||
|
return store.createAccountFromGeth(-1).then(() => {
|
||||||
|
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(GETH_ADDRESSES[0], {
|
||||||
|
timestamp: -1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAccountFromPhrase', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCreateType('fromPhrase');
|
||||||
|
store.setName('some name');
|
||||||
|
store.setPassword('P@55worD');
|
||||||
|
store.setPasswordHint('some hint');
|
||||||
|
store.setPhrase('some phrase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls parity.newAccountFromWallet', () => {
|
||||||
|
return store.createAccountFromPhrase().then(() => {
|
||||||
|
expect(store._api.parity.newAccountFromPhrase).to.have.been.calledWith('some phrase', 'P@55worD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the address', () => {
|
||||||
|
return store.createAccountFromPhrase().then(() => {
|
||||||
|
expect(store.address).to.equal(ADDRESS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account name', () => {
|
||||||
|
return store.createAccountFromPhrase().then(() => {
|
||||||
|
expect(store._api.parity.setAccountName).to.have.been.calledWith(ADDRESS, 'some name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account meta', () => {
|
||||||
|
return store.createAccountFromPhrase(-1).then(() => {
|
||||||
|
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, {
|
||||||
|
passwordHint: 'some hint',
|
||||||
|
timestamp: -1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjusts phrases for Windows', () => {
|
||||||
|
store.setWindowsPhrase(true);
|
||||||
|
return store.createAccountFromPhrase().then(() => {
|
||||||
|
expect(store._api.parity.newAccountFromPhrase).to.have.been.calledWith('some\r phrase\r', 'P@55worD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjusts phrases for Windows (except last word)', () => {
|
||||||
|
store.setWindowsPhrase(true);
|
||||||
|
store.setPhrase('misjudged phrase');
|
||||||
|
return store.createAccountFromPhrase().then(() => {
|
||||||
|
expect(store._api.parity.newAccountFromPhrase).to.have.been.calledWith('misjudged phrase\r', 'P@55worD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAccountFromRaw', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setName('some name');
|
||||||
|
store.setPassword('P@55worD');
|
||||||
|
store.setPasswordHint('some hint');
|
||||||
|
store.setRawKey('rawKey');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls parity.newAccountFromSecret', () => {
|
||||||
|
return store.createAccountFromRaw().then(() => {
|
||||||
|
expect(store._api.parity.newAccountFromSecret).to.have.been.calledWith('rawKey', 'P@55worD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the address', () => {
|
||||||
|
return store.createAccountFromRaw().then(() => {
|
||||||
|
expect(store.address).to.equal(ADDRESS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account name', () => {
|
||||||
|
return store.createAccountFromRaw().then(() => {
|
||||||
|
expect(store._api.parity.setAccountName).to.have.been.calledWith(ADDRESS, 'some name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account meta', () => {
|
||||||
|
return store.createAccountFromRaw(-1).then(() => {
|
||||||
|
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, {
|
||||||
|
passwordHint: 'some hint',
|
||||||
|
timestamp: -1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAccountFromWallet', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setName('some name');
|
||||||
|
store.setPassword('P@55worD');
|
||||||
|
store.setPasswordHint('some hint');
|
||||||
|
store.setWalletJson('json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls parity.newAccountFromWallet', () => {
|
||||||
|
return store.createAccountFromWallet().then(() => {
|
||||||
|
expect(store._api.parity.newAccountFromWallet).to.have.been.calledWith('json', 'P@55worD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the address', () => {
|
||||||
|
return store.createAccountFromWallet().then(() => {
|
||||||
|
expect(store.address).to.equal(ADDRESS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account name', () => {
|
||||||
|
return store.createAccountFromWallet().then(() => {
|
||||||
|
expect(store._api.parity.setAccountName).to.have.been.calledWith(ADDRESS, 'some name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account meta', () => {
|
||||||
|
return store.createAccountFromWallet(-1).then(() => {
|
||||||
|
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, {
|
||||||
|
passwordHint: 'some hint',
|
||||||
|
timestamp: -1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createIdentities', () => {
|
||||||
|
it('creates calls parity.generateSecretPhrase', () => {
|
||||||
|
return store.createIdentities().then(() => {
|
||||||
|
expect(store._api.parity.generateSecretPhrase).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a map of 5 accounts', () => {
|
||||||
|
return store.createIdentities().then((accounts) => {
|
||||||
|
expect(Object.keys(accounts).length).to.equal(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates accounts with an address & phrase', () => {
|
||||||
|
return store.createIdentities().then((accounts) => {
|
||||||
|
Object.keys(accounts).forEach((address) => {
|
||||||
|
const account = accounts[address];
|
||||||
|
|
||||||
|
expect(account.address).to.equal(address);
|
||||||
|
expect(account.phrase).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadAvailableGethAccounts', () => {
|
||||||
|
it('retrieves the list from parity.listGethAccounts', () => {
|
||||||
|
return store.loadAvailableGethAccounts().then(() => {
|
||||||
|
expect(store._api.parity.listGethAccounts).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the available addresses with balances', () => {
|
||||||
|
return store.loadAvailableGethAccounts().then(() => {
|
||||||
|
expect(store.gethAccountsAvailable[0]).to.deep.equal({
|
||||||
|
address: GETH_ADDRESSES[0],
|
||||||
|
balance: '0.00000'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters accounts already available', () => {
|
||||||
|
return store.loadAvailableGethAccounts().then(() => {
|
||||||
|
expect(store.gethAccountsAvailable.length).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,47 +14,73 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ActionDone from 'material-ui/svg-icons/action/done';
|
import { bindActionCreators } from 'redux';
|
||||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
|
||||||
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
|
|
||||||
import PrintIcon from 'material-ui/svg-icons/action/print';
|
|
||||||
|
|
||||||
|
import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg';
|
||||||
|
import { createIdentityImg } from '~/api/util/identity';
|
||||||
|
import { newError } from '~/redux/actions';
|
||||||
import { Button, Modal } from '~/ui';
|
import { Button, Modal } from '~/ui';
|
||||||
|
import { CheckIcon, DoneIcon, NextIcon, PrintIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
import { NewAccount, AccountDetails } from '../CreateAccount';
|
import { NewAccount, AccountDetails } from '../CreateAccount';
|
||||||
|
import print from '../CreateAccount/print';
|
||||||
|
import recoveryPage from '../CreateAccount/recoveryPage.ejs';
|
||||||
|
import CreateStore from '../CreateAccount/store';
|
||||||
|
|
||||||
import Completed from './Completed';
|
import Completed from './Completed';
|
||||||
import TnC from './TnC';
|
import TnC from './TnC';
|
||||||
import Welcome from './Welcome';
|
import Welcome from './Welcome';
|
||||||
|
|
||||||
import { createIdentityImg } from '~/api/util/identity';
|
const STAGE_NAMES = [
|
||||||
import print from '../CreateAccount/print';
|
<FormattedMessage
|
||||||
import recoveryPage from '../CreateAccount/recovery-page.ejs';
|
id='firstRun.title.welcome'
|
||||||
import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg';
|
defaultMessage='welcome'
|
||||||
|
/>,
|
||||||
const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed'];
|
<FormattedMessage
|
||||||
|
id='firstRun.title.terms'
|
||||||
|
defaultMessage='terms'
|
||||||
|
/>,
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.title.newAccount'
|
||||||
|
defaultMessage='new account'
|
||||||
|
/>,
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.title.recovery'
|
||||||
|
defaultMessage='recovery'
|
||||||
|
/>,
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.title.completed'
|
||||||
|
defaultMessage='completed'
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
const BUTTON_LABEL_NEXT = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.button.next'
|
||||||
|
defaultMessage='Next'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
@observer
|
||||||
class FirstRun extends Component {
|
class FirstRun extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired,
|
api: PropTypes.object.isRequired
|
||||||
store: PropTypes.object.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
hasAccounts: PropTypes.bool.isRequired,
|
hasAccounts: PropTypes.bool.isRequired,
|
||||||
visible: PropTypes.bool.isRequired,
|
newError: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired
|
onClose: PropTypes.func.isRequired,
|
||||||
|
visible: PropTypes.bool.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createStore = new CreateStore(this.context.api, {}, false);
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
stage: 0,
|
stage: 0,
|
||||||
name: '',
|
|
||||||
address: '',
|
|
||||||
password: '',
|
|
||||||
phrase: '',
|
|
||||||
canCreate: false,
|
|
||||||
hasAcceptedTnc: false
|
hasAcceptedTnc: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +105,7 @@ class FirstRun extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStage () {
|
renderStage () {
|
||||||
const { address, name, phrase, stage, hasAcceptedTnc } = this.state;
|
const { stage, hasAcceptedTnc } = this.state;
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -96,16 +122,13 @@ class FirstRun extends Component {
|
|||||||
case 2:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<NewAccount
|
<NewAccount
|
||||||
onChange={ this.onChangeDetails }
|
newError={ this.props.newError }
|
||||||
|
store={ this.createStore }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<AccountDetails
|
<AccountDetails store={ this.createStore } />
|
||||||
address={ address }
|
|
||||||
name={ name }
|
|
||||||
phrase={ phrase }
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
@ -116,14 +139,16 @@ class FirstRun extends Component {
|
|||||||
|
|
||||||
renderDialogActions () {
|
renderDialogActions () {
|
||||||
const { hasAccounts } = this.props;
|
const { hasAccounts } = this.props;
|
||||||
const { canCreate, stage, hasAcceptedTnc } = this.state;
|
const { stage, hasAcceptedTnc } = this.state;
|
||||||
|
const { canCreate } = this.createStore;
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case 0:
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowForward /> }
|
icon={ <NextIcon /> }
|
||||||
label='Next'
|
key='next'
|
||||||
|
label={ BUTTON_LABEL_NEXT }
|
||||||
onClick={ this.onNext }
|
onClick={ this.onNext }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -132,8 +157,9 @@ class FirstRun extends Component {
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
disabled={ !hasAcceptedTnc }
|
disabled={ !hasAcceptedTnc }
|
||||||
icon={ <NavigationArrowForward /> }
|
icon={ <NextIcon /> }
|
||||||
label='Next'
|
key='next'
|
||||||
|
label={ BUTTON_LABEL_NEXT }
|
||||||
onClick={ this.onNext }
|
onClick={ this.onNext }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -141,10 +167,15 @@ class FirstRun extends Component {
|
|||||||
case 2:
|
case 2:
|
||||||
const buttons = [
|
const buttons = [
|
||||||
<Button
|
<Button
|
||||||
icon={ <ActionDone /> }
|
|
||||||
label='Create'
|
|
||||||
key='create'
|
|
||||||
disabled={ !canCreate }
|
disabled={ !canCreate }
|
||||||
|
icon={ <CheckIcon /> }
|
||||||
|
key='create'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.button.create'
|
||||||
|
defaultMessage='Create'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={ this.onCreate }
|
onClick={ this.onCreate }
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
@ -152,9 +183,14 @@ class FirstRun extends Component {
|
|||||||
if (hasAccounts) {
|
if (hasAccounts) {
|
||||||
buttons.unshift(
|
buttons.unshift(
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowForward /> }
|
icon={ <NextIcon /> }
|
||||||
label='Skip'
|
|
||||||
key='skip'
|
key='skip'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.button.skip'
|
||||||
|
defaultMessage='Skip'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={ this.skipAccountCreation }
|
onClick={ this.skipAccountCreation }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -165,12 +201,19 @@ class FirstRun extends Component {
|
|||||||
return [
|
return [
|
||||||
<Button
|
<Button
|
||||||
icon={ <PrintIcon /> }
|
icon={ <PrintIcon /> }
|
||||||
label='Print Phrase'
|
key='print'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.button.print'
|
||||||
|
defaultMessage='Print Phrase'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={ this.printPhrase }
|
onClick={ this.printPhrase }
|
||||||
/>,
|
/>,
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowForward /> }
|
icon={ <NextIcon /> }
|
||||||
label='Next'
|
key='next'
|
||||||
|
label={ BUTTON_LABEL_NEXT }
|
||||||
onClick={ this.onNext }
|
onClick={ this.onNext }
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
@ -178,8 +221,14 @@ class FirstRun extends Component {
|
|||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={ <ActionDoneAll /> }
|
icon={ <DoneIcon /> }
|
||||||
label='Close'
|
key='close'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='firstRun.button.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={ this.onClose }
|
onClick={ this.onClose }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -208,38 +257,18 @@ class FirstRun extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeDetails = (valid, { name, address, password, phrase }) => {
|
|
||||||
this.setState({
|
|
||||||
canCreate: valid,
|
|
||||||
name: name,
|
|
||||||
address: address,
|
|
||||||
password: password,
|
|
||||||
phrase: phrase
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreate = () => {
|
onCreate = () => {
|
||||||
const { api } = this.context;
|
this.createStore.setBusy(true);
|
||||||
const { name, phrase, password } = this.state;
|
|
||||||
|
|
||||||
this.setState({
|
return this.createStore
|
||||||
canCreate: false
|
.createAccount()
|
||||||
});
|
|
||||||
|
|
||||||
return api.parity
|
|
||||||
.newAccountFromPhrase(phrase, password)
|
|
||||||
.then((address) => api.parity.setAccountName(address, name))
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.onNext();
|
this.onNext();
|
||||||
|
this.createStore.setBusy(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('onCreate', error);
|
this.createStore.setBusy(false);
|
||||||
|
this.props.newError(error);
|
||||||
this.setState({
|
|
||||||
canCreate: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,22 +276,35 @@ class FirstRun extends Component {
|
|||||||
this.setState({ stage: this.state.stage + 2 });
|
this.setState({ stage: this.state.stage + 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
newError = (error) => {
|
|
||||||
const { store } = this.context;
|
|
||||||
|
|
||||||
store.dispatch({ type: 'newError', error });
|
|
||||||
}
|
|
||||||
|
|
||||||
printPhrase = () => {
|
printPhrase = () => {
|
||||||
const { address, phrase, name } = this.state;
|
const { address, phrase, name } = this.createStore;
|
||||||
const identity = createIdentityImg(address);
|
const identity = createIdentityImg(address);
|
||||||
|
|
||||||
print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo }));
|
print(recoveryPage({
|
||||||
|
address,
|
||||||
|
identity,
|
||||||
|
logo: ParityLogo,
|
||||||
|
name,
|
||||||
|
phrase
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
return { hasAccounts: state.personal.hasAccounts };
|
const { hasAccounts } = state.personal;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasAccounts
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(FirstRun);
|
function mapDispatchToProps (dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
newError
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FirstRun);
|
||||||
|
69
js/src/modals/FirstRun/firstRun.spec.js
Normal file
69
js/src/modals/FirstRun/firstRun.spec.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import FirstRun from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let onClose;
|
||||||
|
|
||||||
|
function createApi () {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRedux () {
|
||||||
|
return {
|
||||||
|
dispatch: sinon.stub(),
|
||||||
|
subscribe: sinon.stub(),
|
||||||
|
getState: () => {
|
||||||
|
return {
|
||||||
|
personal: {
|
||||||
|
hasAccounts: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function render (props = { visible: true }) {
|
||||||
|
onClose = sinon.stub();
|
||||||
|
component = shallow(
|
||||||
|
<FirstRun
|
||||||
|
{ ...props }
|
||||||
|
onClose={ onClose }
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
store: createRedux()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).find('FirstRun').shallow({
|
||||||
|
context: {
|
||||||
|
api: createApi()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/FirstRun', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import AddIcon from 'material-ui/svg-icons/content/add';
|
import AddIcon from 'material-ui/svg-icons/content/add';
|
||||||
|
import AttachFileIcon from 'material-ui/svg-icons/editor/attach-file';
|
||||||
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
||||||
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
||||||
import CloseIcon from 'material-ui/svg-icons/navigation/close';
|
import CloseIcon from 'material-ui/svg-icons/navigation/close';
|
||||||
@ -31,6 +32,8 @@ import LockedIcon from 'material-ui/svg-icons/action/lock';
|
|||||||
import MoveIcon from 'material-ui/svg-icons/action/open-with';
|
import MoveIcon from 'material-ui/svg-icons/action/open-with';
|
||||||
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||||
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||||
|
import PrintIcon from 'material-ui/svg-icons/action/print';
|
||||||
|
import RefreshIcon from 'material-ui/svg-icons/action/autorenew';
|
||||||
import SaveIcon from 'material-ui/svg-icons/content/save';
|
import SaveIcon from 'material-ui/svg-icons/content/save';
|
||||||
import SendIcon from 'material-ui/svg-icons/content/send';
|
import SendIcon from 'material-ui/svg-icons/content/send';
|
||||||
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
||||||
@ -40,6 +43,7 @@ import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
AddIcon,
|
AddIcon,
|
||||||
|
AttachFileIcon,
|
||||||
CancelIcon,
|
CancelIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
@ -56,6 +60,8 @@ export {
|
|||||||
MoveIcon,
|
MoveIcon,
|
||||||
NextIcon,
|
NextIcon,
|
||||||
PrevIcon,
|
PrevIcon,
|
||||||
|
PrintIcon,
|
||||||
|
RefreshIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
SnoozeIcon,
|
SnoozeIcon,
|
||||||
|
@ -23,7 +23,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui';
|
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui';
|
||||||
import Certifications from '~/ui/Certifications';
|
import Certifications from '~/ui/Certifications';
|
||||||
import { nullableProptype } from '~/util/proptypes';
|
import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes';
|
||||||
|
|
||||||
import styles from '../accounts.css';
|
import styles from '../accounts.css';
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export default class Summary extends Component {
|
|||||||
noLink: PropTypes.bool,
|
noLink: PropTypes.bool,
|
||||||
showCertifications: PropTypes.bool,
|
showCertifications: PropTypes.bool,
|
||||||
handleAddSearchToken: PropTypes.func,
|
handleAddSearchToken: PropTypes.func,
|
||||||
owners: nullableProptype(PropTypes.array)
|
owners: nullableProptype(arrayOrObjectProptype())
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
Loading…
Reference in New Issue
Block a user