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:
Jaco Greeff 2017-01-24 16:18:23 +01:00 committed by GitHub
parent 153f2ca2f2
commit 06433033d9
28 changed files with 2825 additions and 988 deletions

View File

@ -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 }
/> />
); );

View File

@ -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;
});
});

View File

@ -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;
} }

View File

@ -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('');
}
} }

View File

@ -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');
});
});
});
});

View File

@ -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);
} }
} }

View File

@ -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');
});
});
});
});

View File

@ -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 });
} }
} }

View 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');
});
});
});
});

View File

@ -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;
} }
}
}

View File

@ -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);
});
} }
} }

View 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');
});
});
});
});

View File

@ -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);
} }
} }

View 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');
});
});
});
});

View File

@ -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);
} }
} }

View 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');
});
});
});
});

View File

@ -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);
} }
} }

View File

@ -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);
});
});
});
});

View File

@ -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);

View 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;
});
});
});

View 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
};

View File

@ -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>

View 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
};

View 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);
});
});
});
});
});

View File

@ -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);

View 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;
});
});

View File

@ -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,

View File

@ -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 = {