+ }
value='fromNew'
/>
+ }
value='fromPhrase'
/>
+ }
value='fromGeth'
/>
+ }
value='fromJSON'
/>
+ }
value='fromPresale'
/>
+ }
value='fromRaw'
/>
@@ -66,6 +97,8 @@ export default class CreationType extends Component {
}
onChange = (event) => {
- this.props.onChange(event.target.value);
+ const { store } = this.props;
+
+ store.setCreateType(event.target.value);
}
}
diff --git a/js/src/modals/CreateAccount/CreationType/creationType.spec.js b/js/src/modals/CreateAccount/CreationType/creationType.spec.js
new file mode 100644
index 000000000..a04f9182d
--- /dev/null
+++ b/js/src/modals/CreateAccount/CreationType/creationType.spec.js
@@ -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
.
+
+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(
+
+ );
+
+ 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');
+ });
+ });
+ });
+});
diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.js b/js/src/modals/CreateAccount/NewAccount/newAccount.js
index 6cab1de02..cb31e2c76 100644
--- a/js/src/modals/CreateAccount/NewAccount/newAccount.js
+++ b/js/src/modals/CreateAccount/NewAccount/newAccount.js
@@ -14,86 +14,113 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see
.
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { IconButton } from 'material-ui';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
-import ActionAutorenew from 'material-ui/svg-icons/action/autorenew';
-import { Form, Input, IdentityIcon } from '~/ui';
-
-import ERRORS from '../errors';
+import { Form, Input, IdentityIcon, PasswordStrength } from '~/ui';
+import { RefreshIcon } from '~/ui/Icons';
import styles from '../createAccount.css';
+@observer
export default class CreateAccount extends Component {
- static contextTypes = {
- api: PropTypes.object.isRequired,
+ static propTypes = {
+ newError: PropTypes.func.isRequired,
store: PropTypes.object.isRequired
}
- static propTypes = {
- onChange: PropTypes.func.isRequired
- }
-
state = {
- accountName: '',
- accountNameError: ERRORS.noName,
accounts: null,
- isValidName: false,
- isValidPass: true,
- passwordHint: '',
- password1: '',
- password1Error: null,
- password2: '',
- password2Error: null,
selectedAddress: ''
}
componentWillMount () {
- this.createIdentities();
- this.props.onChange(false, {});
+ return this.createIdentities();
}
render () {
- const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error } = this.state;
+ const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint } = this.props.store;
return (
@@ -107,22 +134,24 @@ export default class CreateAccount extends Component {
return null;
}
- const buttons = Object.keys(accounts).map((address) => {
- return (
-
- );
- });
+ const buttons = Object
+ .keys(accounts)
+ .map((address) => {
+ return (
+
+ );
+ });
return (
{ buttons }
@@ -136,31 +165,29 @@ export default class CreateAccount extends Component {
return null;
}
- const identities = Object.keys(accounts).map((address) => {
- return (
-
-
-
- );
- });
+ const identities = Object
+ .keys(accounts)
+ .map((address) => {
+ return (
+
+
+
+ );
+ });
return (
@@ -168,122 +195,64 @@ export default class CreateAccount extends Component {
}
createIdentities = () => {
- const { api } = this.context;
+ const { store } = this.props;
- Promise
- .all([
- api.parity.generateSecretPhrase(),
- api.parity.generateSecretPhrase(),
- api.parity.generateSecretPhrase(),
- api.parity.generateSecretPhrase(),
- api.parity.generateSecretPhrase()
- ])
- .then((phrases) => {
- return Promise
- .all(phrases.map((phrase) => api.parity.phraseToAddress(phrase)))
- .then((addresses) => {
- const accounts = {};
+ return store
+ .createIdentities()
+ .then((accounts) => {
+ const selectedAddress = Object.keys(accounts)[0];
+ const { phrase } = accounts[selectedAddress];
- phrases.forEach((phrase, idx) => {
- accounts[addresses[idx]] = {
- address: addresses[idx],
- phrase: phrase
- };
- });
+ store.setAddress(selectedAddress);
+ store.setPhrase(phrase);
- this.setState({
- selectedAddress: addresses[0],
- accounts: accounts
- });
- });
+ this.setState({
+ accounts,
+ selectedAddress
+ });
})
.catch((error) => {
- console.error('createIdentities', error);
- setTimeout(this.createIdentities, 1000);
- this.newError(error);
+ this.props.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) => {
- 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;
}
- this.setState({
- selectedAddress: address
- }, this.updateParent);
- }
+ this.setState({ selectedAddress }, () => {
+ const { phrase } = this.state.accounts[selectedAddress];
- onEditPasswordHint = (event, passwordHint) => {
- this.setState({
- passwordHint
+ store.setAddress(selectedAddress);
+ store.setPhrase(phrase);
});
}
- onEditAccountName = (event) => {
- const accountName = event.target.value;
- let accountNameError = null;
+ onEditPasswordHint = (event, passwordHint) => {
+ const { store } = this.props;
- if (!accountName || !accountName.trim().length) {
- accountNameError = ERRORS.noName;
- }
-
- this.setState({
- accountName,
- accountNameError,
- isValidName: !accountNameError
- }, this.updateParent);
+ store.setPasswordHint(passwordHint);
}
- onEditPassword1 = (event) => {
- const password1 = event.target.value;
- let password2Error = null;
+ onEditAccountName = (event, name) => {
+ const { store } = this.props;
- if (password1 !== this.state.password2) {
- password2Error = ERRORS.noMatchPassword;
- }
-
- this.setState({
- password1,
- password1Error: null,
- password2Error,
- isValidPass: !password2Error
- }, this.updateParent);
+ store.setName(name);
}
- onEditPassword2 = (event) => {
- const password2 = event.target.value;
- let password2Error = null;
+ onEditPassword = (event, password) => {
+ const { store } = this.props;
- if (password2 !== this.state.password1) {
- password2Error = ERRORS.noMatchPassword;
- }
-
- this.setState({
- password2,
- password2Error,
- isValidPass: !password2Error
- }, this.updateParent);
+ store.setPassword(password);
}
- newError = (error) => {
- const { store } = this.context;
+ onEditPasswordRepeat = (event, password) => {
+ const { store } = this.props;
- store.dispatch({ type: 'newError', error });
+ store.setPasswordRepeat(password);
}
}
diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js b/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
new file mode 100644
index 000000000..25a7d52bc
--- /dev/null
+++ b/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
@@ -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
.
+
+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(
+
,
+ {
+ 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');
+ });
+ });
+ });
+});
diff --git a/js/src/modals/CreateAccount/NewGeth/newGeth.css b/js/src/modals/CreateAccount/NewGeth/newGeth.css
index 94c143b03..f16226a4d 100644
--- a/js/src/modals/CreateAccount/NewGeth/newGeth.css
+++ b/js/src/modals/CreateAccount/NewGeth/newGeth.css
@@ -15,29 +15,28 @@
/* along with Parity. If not, see
.
*/
.list {
-}
-
-.list input+div>div {
- top: 13px;
+ input+div>div {
+ top: 13px;
+ }
}
.selection {
display: inline-block;
margin-bottom: 0.5em;
-}
-.selection .icon {
- display: inline-block;
-}
+ .icon {
+ display: inline-block;
+ }
-.selection .detail {
- display: inline-block;
-}
+ .detail {
+ display: inline-block;
-.detail .address {
- color: #aaa;
-}
+ .address {
+ color: #aaa;
+ }
-.detail .balance {
- font-family: 'Roboto Mono', monospace;
+ .balance {
+ font-family: 'Roboto Mono', monospace;
+ }
+ }
}
diff --git a/js/src/modals/CreateAccount/NewGeth/newGeth.js b/js/src/modals/CreateAccount/NewGeth/newGeth.js
index 8566f76b4..4c40f3b13 100644
--- a/js/src/modals/CreateAccount/NewGeth/newGeth.js
+++ b/js/src/modals/CreateAccount/NewGeth/newGeth.js
@@ -14,63 +14,68 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see
.
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { Checkbox } from 'material-ui';
import { IdentityIcon } from '~/ui';
import styles from './newGeth.css';
+@observer
export default class NewGeth extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
- accounts: PropTypes.object.isRequired,
- onChange: PropTypes.func.isRequired
- }
-
- state = {
- available: []
- }
-
- componentDidMount () {
- this.loadAvailable();
+ store: PropTypes.object.isRequired
}
render () {
- const { available } = this.state;
+ const { gethAccountsAvailable, gethAddresses } = this.props.store;
- if (!available.length) {
+ if (!gethAccountsAvailable.length) {
return (
-
There are currently no importable keys available from the Geth keystore, which are not already available on your Parity instance
+
+
+
);
}
- const checkboxes = available.map((account) => {
+ const checkboxes = gethAccountsAvailable.map((account) => {
+ const onSelect = (event) => this.onSelectAddress(event, account.address);
+
const label = (
-
{ account.address }
-
{ account.balance } ETH
+
+ { account.address }
+
+
+ { account.balance } ETH
+
);
return (
);
});
@@ -82,51 +87,9 @@ export default class NewGeth extends Component {
);
}
- onSelect = (event, checked) => {
- const address = event.target.getAttribute('data-address');
+ onSelectAddress = (event, address) => {
+ const { store } = this.props;
- if (!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);
- });
+ store.selectGethAccount(address);
}
}
diff --git a/js/src/modals/CreateAccount/NewGeth/newGeth.spec.js b/js/src/modals/CreateAccount/NewGeth/newGeth.spec.js
new file mode 100644
index 000000000..d929ce62c
--- /dev/null
+++ b/js/src/modals/CreateAccount/NewGeth/newGeth.spec.js
@@ -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
.
+
+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(
+
+ );
+ 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');
+ });
+ });
+ });
+});
diff --git a/js/src/modals/CreateAccount/NewImport/newImport.js b/js/src/modals/CreateAccount/NewImport/newImport.js
index 6e064770f..1de7ee9d1 100644
--- a/js/src/modals/CreateAccount/NewImport/newImport.js
+++ b/js/src/modals/CreateAccount/NewImport/newImport.js
@@ -14,91 +14,114 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see
.
+import { FloatingActionButton } from 'material-ui';
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
-import { FloatingActionButton } from 'material-ui';
-import EditorAttachFile from 'material-ui/svg-icons/editor/attach-file';
+import { FormattedMessage } from 'react-intl';
import { Form, Input } from '~/ui';
-
-import ERRORS from '../errors';
+import { AttachFileIcon } from '~/ui/Icons';
import styles from '../createAccount.css';
-const FAKEPATH = 'C:\\fakepath\\';
const STYLE_HIDDEN = { display: 'none' };
+@observer
export default class NewImport extends Component {
static propTypes = {
- onChange: PropTypes.func.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, {});
+ store: PropTypes.object.isRequired
}
render () {
+ const { name, nameError, password, passwordHint, walletFile, walletFileError } = this.props.store;
+
return (
);
}
- updateParent = () => {
- const { isValidName, isValidPass, isValidKey, accountName, passwordHint, password1, rawKey } = this.state;
- const isValid = isValidName && isValidPass && isValidKey;
+ onEditName = (event, name) => {
+ const { store } = this.props;
- this.props.onChange(isValid, {
- name: accountName,
- passwordHint,
- password: password1,
- rawKey
- });
+ store.setName(name);
}
- onEditPasswordHint = (event, value) => {
- this.setState({
- passwordHint: value
- });
+ onEditPasswordHint = (event, passwordHint) => {
+ const { store } = this.props;
+
+ store.setPasswordHint(passwordHint);
}
- onEditKey = (event) => {
- const { api } = this.context;
- const rawKey = event.target.value;
- let rawKeyError = null;
+ onEditPassword = (event, password) => {
+ const { store } = this.props;
- if (!rawKey || !rawKey.trim().length) {
- rawKeyError = ERRORS.noKey;
- } else if (rawKey.substr(0, 2) !== '0x' || rawKey.substr(2).length !== 64 || !api.util.isHex(rawKey)) {
- rawKeyError = ERRORS.invalidKey;
- }
-
- this.setState({
- rawKey,
- rawKeyError,
- isValidKey: !rawKeyError
- }, this.updateParent);
+ store.setPassword(password);
}
- onEditAccountName = (event) => {
- const accountName = event.target.value;
- let accountNameError = null;
+ onEditPasswordRepeat = (event, password) => {
+ const { store } = this.props;
- if (!accountName || !accountName.trim().length) {
- accountNameError = ERRORS.noName;
- }
-
- this.setState({
- accountName,
- accountNameError,
- isValidName: !accountNameError
- }, this.updateParent);
+ store.setPasswordRepeat(password);
}
- onEditPassword1 = (event) => {
- const password1 = event.target.value;
- let password2Error = null;
+ onEditKey = (event, rawKey) => {
+ const { store } = this.props;
- 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);
+ store.setRawKey(rawKey);
}
}
diff --git a/js/src/modals/CreateAccount/RawKey/rawKey.spec.js b/js/src/modals/CreateAccount/RawKey/rawKey.spec.js
new file mode 100644
index 000000000..06ff0c475
--- /dev/null
+++ b/js/src/modals/CreateAccount/RawKey/rawKey.spec.js
@@ -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
.
+
+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(
+
+ );
+ 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');
+ });
+ });
+ });
+});
diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js
index da91b4ba2..1da1e6dba 100644
--- a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js
+++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js
@@ -14,183 +14,165 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see
.
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { Checkbox } from 'material-ui';
-import { Form, Input } from '~/ui';
+import { Form, Input, PasswordStrength } from '~/ui';
import styles from '../createAccount.css';
-import ERRORS from '../errors';
-
+@observer
export default class RecoveryPhrase extends Component {
static propTypes = {
- onChange: PropTypes.func.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, {});
+ store: PropTypes.object.isRequired
}
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 (
);
}
- 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) => {
- this.setState({
- windowsPhrase: !this.state.windowsPhrase
- }, this.updateParent);
+ const { store } = this.props;
+
+ store.setWindowsPhrase(!store.isWindowsPhrase);
}
- onEditPhrase = (event) => {
- const recoveryPhrase = event.target.value
- .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
+ onEditPhrase = (event, phrase) => {
+ const { store } = this.props;
- const phraseParts = recoveryPhrase
- .split(' ')
- .map((part) => part.trim())
- .filter((part) => part.length);
-
- this.setState({
- recoveryPhrase: phraseParts.join(' '),
- recoveryPhraseError: null,
- isValidPhrase: true
- }, this.updateParent);
+ store.setPhrase(phrase);
}
- onEditAccountName = (event) => {
- const accountName = event.target.value;
- let accountNameError = null;
+ onEditName = (event, name) => {
+ const { store } = this.props;
- if (!accountName || !accountName.trim().length) {
- accountNameError = ERRORS.noName;
- }
-
- this.setState({
- accountName,
- accountNameError,
- isValidName: !accountNameError
- }, this.updateParent);
+ store.setName(name);
}
- onEditPassword1 = (event) => {
- const password1 = event.target.value;
- let password2Error = null;
+ onEditPassword = (event, password) => {
+ const { store } = this.props;
- if (password1 !== this.state.password2) {
- password2Error = ERRORS.noMatchPassword;
- }
-
- this.setState({
- password1,
- password1Error: null,
- password2Error,
- isValidPass: !password2Error
- }, this.updateParent);
+ store.setPassword(password);
}
- onEditPassword2 = (event) => {
- const password2 = event.target.value;
- let password2Error = null;
+ onEditPasswordRepeat = (event, password) => {
+ const { store } = this.props;
- if (password2 !== this.state.password1) {
- password2Error = ERRORS.noMatchPassword;
- }
+ store.setPasswordRepeat(password);
+ }
- this.setState({
- password2,
- password2Error,
- isValidPass: !password2Error
- }, this.updateParent);
+ onEditPasswordHint = (event, passwordHint) => {
+ const { store } = this.props;
+
+ store.setPasswordHint(passwordHint);
}
}
diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.spec.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.spec.js
new file mode 100644
index 000000000..cd4f3ad45
--- /dev/null
+++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.spec.js
@@ -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
.
+
+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(
+
+ );
+ 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);
+ });
+ });
+ });
+});
diff --git a/js/src/modals/CreateAccount/createAccount.js b/js/src/modals/CreateAccount/createAccount.js
index 7c00510d4..334a278f6 100644
--- a/js/src/modals/CreateAccount/createAccount.js
+++ b/js/src/modals/CreateAccount/createAccount.js
@@ -14,17 +14,17 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see
.
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
-import ActionDone from 'material-ui/svg-icons/action/done';
-import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
-import ContentClear from 'material-ui/svg-icons/content/clear';
-import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
-import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
-import PrintIcon from 'material-ui/svg-icons/action/print';
-
-import { Button, Modal, Warning } from '~/ui';
+import { createIdentityImg } from '~/api/util/identity';
+import { newError } from '~/redux/actions';
+import { Button, Modal } from '~/ui';
+import { CancelIcon, CheckIcon, DoneIcon, NextIcon, PrevIcon, PrintIcon } from '~/ui/Icons';
+import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg';
import AccountDetails from './AccountDetails';
import AccountDetailsGeth from './AccountDetailsGeth';
@@ -34,405 +34,271 @@ import NewGeth from './NewGeth';
import NewImport from './NewImport';
import RawKey from './RawKey';
import RecoveryPhrase from './RecoveryPhrase';
-
-import { createIdentityImg } from '~/api/util/identity';
+import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store';
import print from './print';
-import recoveryPage from './recovery-page.ejs';
-import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg';
+import recoveryPage from './recoveryPage.ejs';
const TITLES = {
- type: 'creation type',
- create: 'create account',
- info: 'account information',
- import: 'import wallet'
+ type: (
+
+ ),
+ create: (
+
+ ),
+ info: (
+
+ ),
+ import: (
+
+ )
};
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info];
const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info];
-export default class CreateAccount extends Component {
+@observer
+class CreateAccount extends Component {
static contextTypes = {
- api: PropTypes.object.isRequired,
- store: PropTypes.object.isRequired
+ api: PropTypes.object.isRequired
}
static propTypes = {
accounts: PropTypes.object.isRequired,
+ newError: PropTypes.func.isRequired,
onClose: PropTypes.func,
onUpdate: PropTypes.func
}
- state = {
- address: null,
- name: null,
- passwordHint: null,
- password: null,
- phrase: null,
- windowsPhrase: false,
- rawKey: null,
- json: null,
- canCreate: false,
- createType: null,
- gethAddresses: [],
- stage: 0
- }
+ store = new Store(this.context.api, this.props.accounts);
render () {
- const { createType, stage } = this.state;
- const steps = createType === 'fromNew'
- ? STAGE_NAMES
- : STAGE_IMPORT;
+ const { createType, stage } = this.store;
return (
- { this.renderWarning() }
{ this.renderPage() }
);
}
renderPage () {
- const { createType, stage } = this.state;
- const { accounts } = this.props;
+ const { createType, stage } = this.store;
switch (stage) {
- case 0:
+ case STAGE_SELECT_TYPE:
return (
-
+
);
- case 1:
+ case STAGE_CREATE:
if (createType === 'fromNew') {
return (
-
+
);
}
if (createType === 'fromGeth') {
return (
-
+
);
}
if (createType === 'fromPhrase') {
return (
-
+
);
}
if (createType === 'fromRaw') {
return (
-
+
);
}
return (
-
+
);
- case 2:
+ case STAGE_INFO:
if (createType === 'fromGeth') {
return (
-
+
);
}
return (
-
+
);
}
}
renderDialogActions () {
- const { createType, stage } = this.state;
+ const { createType, canCreate, isBusy, stage } = this.store;
+
+ const cancelBtn = (
+
}
+ key='cancel'
+ label={
+
+ }
+ onClick={ this.onClose }
+ />
+ );
switch (stage) {
- case 0:
+ case STAGE_SELECT_TYPE:
return [
+ cancelBtn,
}
- label='Cancel'
- onClick={ this.onClose }
- />,
-
}
- label='Next'
- onClick={ this.onNext }
+ icon={
}
+ key='next'
+ label={
+
+ }
+ onClick={ this.store.nextStage }
/>
];
- case 1:
- const createLabel = createType === 'fromNew'
- ? 'Create'
- : 'Import';
+ case STAGE_CREATE:
return [
+ cancelBtn,
}
- label='Cancel'
- onClick={ this.onClose }
+ icon={
}
+ key='back'
+ label={
+
+ }
+ onClick={ this.store.prevStage }
/>,
}
- label='Back'
- onClick={ this.onPrev }
- />,
-
}
- label={ createLabel }
- disabled={ !this.state.canCreate }
+ disabled={ !canCreate || isBusy }
+ icon={
}
+ key='create'
+ label={
+ createType === 'fromNew'
+ ? (
+
+ )
+ : (
+
+ )
+ }
onClick={ this.onCreate }
/>
];
- case 2:
+ case STAGE_INFO:
return [
- createType === 'fromNew' || createType === 'fromPhrase' ? (
-
}
- label='Print Phrase'
- onClick={ this.printPhrase }
- />
- ) : null,
+ ['fromNew', 'fromPhrase'].includes(createType)
+ ? (
+
}
+ key='print'
+ label={
+
+ }
+ onClick={ this.printPhrase }
+ />
+ )
+ : null,
}
- label='Close'
+ icon={
}
+ key='close'
+ label={
+
+ }
onClick={ this.onClose }
/>
];
}
}
- renderWarning () {
- const { createType, stage } = this.state;
-
- if (stage !== 1 || ['fromJSON', 'fromPresale'].includes(createType)) {
- return null;
- }
-
- return (
-
- }
- />
- );
- }
-
- onNext = () => {
- this.setState({
- stage: this.state.stage + 1
- });
- }
-
- onPrev = () => {
- this.setState({
- stage: this.state.stage - 1
- });
- }
-
onCreate = () => {
- const { createType, windowsPhrase } = this.state;
- const { api } = this.context;
+ this.store.setBusy(true);
- this.setState({
- canCreate: false
- });
-
- 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(() => {
- this.onNext();
- this.props.onUpdate && this.props.onUpdate();
- })
- .catch((error) => {
- console.error('onCreate', 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
- }));
- })
+ return this.store
+ .createAccount()
.then(() => {
- this.onNext();
+ this.store.setBusy(false);
+ this.store.nextStage();
this.props.onUpdate && this.props.onUpdate();
})
.catch((error) => {
- console.error('onCreate', error);
-
- this.setState({
- canCreate: true
- });
-
- this.newError(error);
+ this.store.setBusy(false);
+ this.props.newError(error);
});
}
onClose = () => {
- this.setState({
- stage: 0,
- canCreate: false
- }, () => {
- 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 });
+ this.props.onClose && this.props.onClose();
}
printPhrase = () => {
- const { address, phrase, name } = this.state;
+ const { address, name, phrase } = this.store;
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);
diff --git a/js/src/modals/CreateAccount/createAccount.spec.js b/js/src/modals/CreateAccount/createAccount.spec.js
new file mode 100644
index 000000000..1bfc622ad
--- /dev/null
+++ b/js/src/modals/CreateAccount/createAccount.spec.js
@@ -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
.
+
+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(
+
,
+ {
+ context: {
+ store: createRedux()
+ }
+ }
+ ).find('CreateAccount').shallow({
+ context: { api }
+ });
+
+ return component;
+}
+
+describe('modals/CreateAccount', () => {
+ describe('rendering', () => {
+ it('renders with defaults', () => {
+ expect(render()).to.be.ok;
+ });
+ });
+});
diff --git a/js/src/modals/CreateAccount/createAccount.test.js b/js/src/modals/CreateAccount/createAccount.test.js
new file mode 100644
index 000000000..02e22c669
--- /dev/null
+++ b/js/src/modals/CreateAccount/createAccount.test.js
@@ -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
.
+
+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
+};
diff --git a/js/src/modals/CreateAccount/recovery-page.ejs b/js/src/modals/CreateAccount/recoveryPage.ejs
similarity index 99%
rename from js/src/modals/CreateAccount/recovery-page.ejs
rename to js/src/modals/CreateAccount/recoveryPage.ejs
index bcd1a8b42..460948d7b 100644
--- a/js/src/modals/CreateAccount/recovery-page.ejs
+++ b/js/src/modals/CreateAccount/recoveryPage.ejs
@@ -1,5 +1,4 @@
-
Recovery phrase for <%= name %>
diff --git a/js/src/modals/CreateAccount/store.js b/js/src/modals/CreateAccount/store.js
new file mode 100644
index 000000000..53f859622
--- /dev/null
+++ b/js/src/modals/CreateAccount/store.js
@@ -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
.
+
+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
+};
diff --git a/js/src/modals/CreateAccount/store.spec.js b/js/src/modals/CreateAccount/store.spec.js
new file mode 100644
index 000000000..4a6cf2bb9
--- /dev/null
+++ b/js/src/modals/CreateAccount/store.spec.js
@@ -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
.
+
+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);
+ });
+ });
+ });
+ });
+});
diff --git a/js/src/modals/FirstRun/firstRun.js b/js/src/modals/FirstRun/firstRun.js
index 1e2c522ce..2a22d3c08 100644
--- a/js/src/modals/FirstRun/firstRun.js
+++ b/js/src/modals/FirstRun/firstRun.js
@@ -14,47 +14,73 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see
.
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import ActionDone from 'material-ui/svg-icons/action/done';
-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 { bindActionCreators } from 'redux';
+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 { CheckIcon, DoneIcon, NextIcon, PrintIcon } from '~/ui/Icons';
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 TnC from './TnC';
import Welcome from './Welcome';
-import { createIdentityImg } from '~/api/util/identity';
-import print from '../CreateAccount/print';
-import recoveryPage from '../CreateAccount/recovery-page.ejs';
-import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg';
-
-const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed'];
+const STAGE_NAMES = [
+
,
+
,
+
,
+
,
+
+];
+const BUTTON_LABEL_NEXT = (
+
+);
+@observer
class FirstRun extends Component {
static contextTypes = {
- api: PropTypes.object.isRequired,
- store: PropTypes.object.isRequired
+ api: PropTypes.object.isRequired
}
static propTypes = {
hasAccounts: PropTypes.bool.isRequired,
- visible: PropTypes.bool.isRequired,
- onClose: PropTypes.func.isRequired
+ newError: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ visible: PropTypes.bool.isRequired
}
+ createStore = new CreateStore(this.context.api, {}, false);
+
state = {
stage: 0,
- name: '',
- address: '',
- password: '',
- phrase: '',
- canCreate: false,
hasAcceptedTnc: false
}
@@ -79,7 +105,7 @@ class FirstRun extends Component {
}
renderStage () {
- const { address, name, phrase, stage, hasAcceptedTnc } = this.state;
+ const { stage, hasAcceptedTnc } = this.state;
switch (stage) {
case 0:
@@ -96,16 +122,13 @@ class FirstRun extends Component {
case 2:
return (
);
case 3:
return (
-
+
);
case 4:
return (
@@ -116,14 +139,16 @@ class FirstRun extends Component {
renderDialogActions () {
const { hasAccounts } = this.props;
- const { canCreate, stage, hasAcceptedTnc } = this.state;
+ const { stage, hasAcceptedTnc } = this.state;
+ const { canCreate } = this.createStore;
switch (stage) {
case 0:
return (
}
- label='Next'
+ icon={
}
+ key='next'
+ label={ BUTTON_LABEL_NEXT }
onClick={ this.onNext }
/>
);
@@ -132,8 +157,9 @@ class FirstRun extends Component {
return (
}
- label='Next'
+ icon={
}
+ key='next'
+ label={ BUTTON_LABEL_NEXT }
onClick={ this.onNext }
/>
);
@@ -141,10 +167,15 @@ class FirstRun extends Component {
case 2:
const buttons = [
}
- label='Create'
- key='create'
disabled={ !canCreate }
+ icon={
}
+ key='create'
+ label={
+
+ }
onClick={ this.onCreate }
/>
];
@@ -152,9 +183,14 @@ class FirstRun extends Component {
if (hasAccounts) {
buttons.unshift(
}
- label='Skip'
+ icon={
}
key='skip'
+ label={
+
+ }
onClick={ this.skipAccountCreation }
/>
);
@@ -165,12 +201,19 @@ class FirstRun extends Component {
return [
}
- label='Print Phrase'
+ key='print'
+ label={
+
+ }
onClick={ this.printPhrase }
/>,
}
- label='Next'
+ icon={
}
+ key='next'
+ label={ BUTTON_LABEL_NEXT }
onClick={ this.onNext }
/>
];
@@ -178,8 +221,14 @@ class FirstRun extends Component {
case 4:
return (
}
- label='Close'
+ icon={
}
+ key='close'
+ label={
+
+ }
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 = () => {
- const { api } = this.context;
- const { name, phrase, password } = this.state;
+ this.createStore.setBusy(true);
- this.setState({
- canCreate: false
- });
-
- return api.parity
- .newAccountFromPhrase(phrase, password)
- .then((address) => api.parity.setAccountName(address, name))
+ return this.createStore
+ .createAccount()
.then(() => {
this.onNext();
+ this.createStore.setBusy(false);
})
.catch((error) => {
- console.error('onCreate', error);
-
- this.setState({
- canCreate: true
- });
-
- this.newError(error);
+ this.createStore.setBusy(false);
+ this.props.newError(error);
});
}
@@ -247,22 +276,35 @@ class FirstRun extends Component {
this.setState({ stage: this.state.stage + 2 });
}
- newError = (error) => {
- const { store } = this.context;
-
- store.dispatch({ type: 'newError', error });
- }
-
printPhrase = () => {
- const { address, phrase, name } = this.state;
+ const { address, phrase, name } = this.createStore;
const identity = createIdentityImg(address);
- print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo }));
+ print(recoveryPage({
+ address,
+ identity,
+ logo: ParityLogo,
+ name,
+ phrase
+ }));
}
}
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);
diff --git a/js/src/modals/FirstRun/firstRun.spec.js b/js/src/modals/FirstRun/firstRun.spec.js
new file mode 100644
index 000000000..4774f09d7
--- /dev/null
+++ b/js/src/modals/FirstRun/firstRun.spec.js
@@ -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
.
+
+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(
+
,
+ {
+ context: {
+ store: createRedux()
+ }
+ }
+ ).find('FirstRun').shallow({
+ context: {
+ api: createApi()
+ }
+ });
+
+ return component;
+}
+
+describe('modals/FirstRun', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+});
diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js
index fc3b67391..575b03696 100644
--- a/js/src/ui/Icons/index.js
+++ b/js/src/ui/Icons/index.js
@@ -15,6 +15,7 @@
// along with Parity. If not, see
.
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 CheckIcon from 'material-ui/svg-icons/navigation/check';
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 NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
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 SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
@@ -40,6 +43,7 @@ import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
export {
AddIcon,
+ AttachFileIcon,
CancelIcon,
CheckIcon,
CloseIcon,
@@ -56,6 +60,8 @@ export {
MoveIcon,
NextIcon,
PrevIcon,
+ PrintIcon,
+ RefreshIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js
index 8849458b6..6f1b4d1b0 100644
--- a/js/src/views/Accounts/Summary/summary.js
+++ b/js/src/views/Accounts/Summary/summary.js
@@ -23,7 +23,7 @@ import { FormattedMessage } from 'react-intl';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui';
import Certifications from '~/ui/Certifications';
-import { nullableProptype } from '~/util/proptypes';
+import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes';
import styles from '../accounts.css';
@@ -40,7 +40,7 @@ export default class Summary extends Component {
noLink: PropTypes.bool,
showCertifications: PropTypes.bool,
handleAddSearchToken: PropTypes.func,
- owners: nullableProptype(PropTypes.array)
+ owners: nullableProptype(arrayOrObjectProptype())
};
static defaultProps = {