diff --git a/js/src/modals/ExportAccount/exportAccount.js b/js/src/modals/ExportAccount/exportAccount.js
new file mode 100644
index 000000000..48faaaec3
--- /dev/null
+++ b/js/src/modals/ExportAccount/exportAccount.js
@@ -0,0 +1,177 @@
+// Copyright 2015-2017 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 { 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 { newError } from '~/redux/actions';
+import { personalAccountsInfo } from '~/redux/providers/personalActions';
+import { AccountCard, Button, Portal, SelectionList } from '~/ui';
+import { CancelIcon, CheckIcon } from '~/ui/Icons';
+import ExportInput from './exportInput';
+import ExportStore from './exportStore';
+
+@observer
+class ExportAccount extends Component {
+ static contextTypes = {
+ api: PropTypes.object.isRequired
+ };
+
+ static propTypes = {
+ accounts: PropTypes.object.isRequired,
+ balances: PropTypes.object.isRequired,
+ newError: PropTypes.func.isRequired,
+ personalAccountsInfo: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired
+ };
+
+ componentWillMount () {
+ const { accounts, newError } = this.props;
+
+ this.exportStore = new ExportStore(this.context.api, accounts, newError, null);
+ }
+
+ render () {
+ const { canExport } = this.exportStore;
+
+ return (
+ }
+ key='cancel'
+ label={
+
+ }
+ onClick={ this.onClose }
+ />,
+ }
+ key='execute'
+ label={
+
+ }
+ onClick={ this.onExport }
+ />
+ ] }
+ onClose={ this.onClose }
+ open
+ title={
+
+ }
+ >
+ { this.renderList() }
+
+ );
+ }
+
+ renderList () {
+ const { accounts } = this.props;
+
+ const { selectedAccounts } = this.exportStore;
+
+ const accountList = Object.values(accounts)
+ .filter((account) => account.uuid)
+ .map((account) => {
+ account.checked = !!(selectedAccounts[account.address]);
+
+ return account;
+ });
+
+ return (
+
+ );
+ }
+
+ renderAccount = (account) => {
+ const { balances } = this.props;
+ const balance = balances[account.address];
+ const { changePassword, getPassword } = this.exportStore;
+ const password = getPassword(account);
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ onSelect = (account) => {
+ this.exportStore.toggleSelectedAccount(account.address);
+ }
+
+ onClick = (address) => {
+ this.exportStore.onClick(address);
+ }
+
+ onClose = () => {
+ this.props.onClose();
+ }
+
+ onExport = () => {
+ this.exportStore.onExport();
+ }
+}
+
+function mapStateToProps (state) {
+ const { balances } = state;
+ const { accounts } = state.personal;
+
+ return {
+ accounts,
+ balances
+ };
+}
+
+function mapDispatchToProps (dispatch) {
+ return bindActionCreators({
+ newError,
+ personalAccountsInfo
+ }, dispatch);
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ExportAccount);
diff --git a/js/src/modals/ExportAccount/exportAccount.spec.js b/js/src/modals/ExportAccount/exportAccount.spec.js
new file mode 100644
index 000000000..7f185cc07
--- /dev/null
+++ b/js/src/modals/ExportAccount/exportAccount.spec.js
@@ -0,0 +1,76 @@
+// Copyright 2015-2017 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 ExportAccount from './';
+
+const ADDRESS = '0x0123456789012345678901234567890123456789';
+
+let component;
+let NEWERROR;
+let PAI;
+let ONCLOSE;
+
+let reduxStore;
+
+function createReduxStore () {
+ reduxStore = {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ balances: {
+ balances: {
+ [ADDRESS]: {}
+ }
+ },
+ personal: {
+ accounts: {
+ [ADDRESS]: {
+ address: ADDRESS
+ }
+ }
+ }
+ };
+ }
+ };
+
+ return reduxStore;
+}
+
+function render () {
+ component = shallow(
+ ,
+ {
+ context: { api: {}, store: createReduxStore() }
+ }
+ );
+
+ return component;
+}
+
+describe('CreateExportModal', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+});
diff --git a/js/src/modals/ExportAccount/exportInput/exportInput.js b/js/src/modals/ExportAccount/exportInput/exportInput.js
new file mode 100644
index 000000000..07673b29b
--- /dev/null
+++ b/js/src/modals/ExportAccount/exportInput/exportInput.js
@@ -0,0 +1,61 @@
+// Copyright 2015-2017 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 React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Input } from '~/ui/Form';
+
+export default class ExportInput extends Component {
+ static propTypes = {
+ account: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.string
+ };
+
+ render () {
+ const { value, onChange } = this.props;
+
+ return (
+
+ }
+ hint={
+
+ }
+ value={ value }
+ onClick={ this.onClick }
+ onChange={ onChange }
+ />
+ );
+ }
+
+ onClick = (event) => {
+ const { account, onClick } = this.props;
+
+ event.stopPropagation();
+
+ onClick && onClick(account.address);
+ }
+}
diff --git a/js/src/modals/ExportAccount/exportInput/index.js b/js/src/modals/ExportAccount/exportInput/index.js
new file mode 100644
index 000000000..fb685bf6b
--- /dev/null
+++ b/js/src/modals/ExportAccount/exportInput/index.js
@@ -0,0 +1,17 @@
+// Copyright 2015-2017 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 .
+
+export default from './exportInput';
diff --git a/js/src/modals/ExportAccount/exportStore.js b/js/src/modals/ExportAccount/exportStore.js
new file mode 100644
index 000000000..cba8d00a9
--- /dev/null
+++ b/js/src/modals/ExportAccount/exportStore.js
@@ -0,0 +1,106 @@
+// Copyright 2015-2017 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, observable } from 'mobx';
+import FileSaver from 'file-saver';
+
+export default class ExportStore {
+ @observable canExport = false;
+ @observable selectedAccount = '';
+ @observable selectedAccounts = {};
+ @observable passwordInputs = {};
+
+ constructor (api, accounts, newError, address) {
+ this.accounts = accounts;
+ this._api = api;
+ this._newError = newError;
+ if (address) {
+ this.selectedAccounts[address] = true;
+ this.selectedAccount = address;
+ }
+ }
+
+ @action changePassword = (event, password) => {
+ this.passwordInputs[this.selectedAccount] = password;
+ }
+
+ @action getPassword = (address) => {
+ return this.passwordInputs[address];
+ }
+
+ @action onClick = (address) => {
+ this.selectedAccount = address;
+ }
+
+ @action resetAccountValue = () => {
+ this.passwordInputs[this.selectedAccount] = '';
+ }
+
+ @action setAccounts = (accounts) => {
+ this.accounts = accounts;
+ }
+
+ @action setSelectedAccount = (addr) => {
+ this.selectedAccounts[addr] = true;
+ this.canExport = true;
+ }
+
+ @action toggleSelectedAccount = (addr) => {
+ if (this.selectedAccounts[addr]) {
+ delete this.selectedAccounts[addr];
+ } else {
+ this.selectedAccounts[addr] = true;
+ }
+ this.canExport = false;
+ Object
+ .keys(this.selectedAccounts)
+ .forEach((address) => {
+ if (this.selectedAccounts[address]) {
+ this.canExport = true;
+ }
+ });
+ }
+
+ onExport = (event) => {
+ const { parity } = this._api;
+ const accounts = Object.keys(this.selectedAccounts);
+
+ accounts.forEach((address) => {
+ let password = this.passwordInputs[address];
+
+ parity
+ .exportAccount(address, password)
+ .then((content) => {
+ const text = JSON.stringify(content, null, 4);
+ const blob = new Blob([ text ], { type: 'application/json' });
+ const filename = this.accounts[address].uuid;
+
+ FileSaver.saveAs(blob, `${filename}.json`);
+
+ this.accountValue = '';
+ if (event) { event(); }
+ })
+ .catch((err) => {
+ const { name, meta } = this.accounts[address];
+ const { passwordHint } = meta;
+
+ this._newError({
+ message: `[${err.code}] Account "${name}" - Incorrect password. (Password Hint: ${passwordHint})`
+ });
+ });
+ });
+ }
+}
diff --git a/js/src/modals/ExportAccount/exportStore.spec.js b/js/src/modals/ExportAccount/exportStore.spec.js
new file mode 100644
index 000000000..08dcbddb8
--- /dev/null
+++ b/js/src/modals/ExportAccount/exportStore.spec.js
@@ -0,0 +1,133 @@
+// Copyright 2015-2017 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 ExportStore from './exportStore';
+
+const ADDRESS = '0x00000123456789abcdef123456789abcdef123456789abcdef';
+const ADDRESS_2 = '0x123456789abcdef123456789abcdef123456789abcdef00000';
+const ACCOUNTS = { ADDRESS: {}, ADDRESS_2: {} };
+
+let api;
+let AccountStore;
+
+function createApi () {
+ return {
+ eth: {
+ },
+ parity: {
+ exportAccount: sinon.stub().resolves({})
+ }
+ };
+}
+
+function createMultiAccountStore (loadGeth) {
+ api = createApi();
+ AccountStore = new ExportStore(api, ACCOUNTS, null, null);
+
+ return AccountStore;
+}
+
+describe('modals/exportAccount/Store', () => {
+ beforeEach(() => {
+ createMultiAccountStore();
+ });
+
+ describe('constructor', () => {
+ it('insert api', () => {
+ expect(AccountStore._api).to.deep.equal(api);
+ });
+
+ it('insert accounts', () => {
+ expect(AccountStore.accounts).to.deep.equal(ACCOUNTS);
+ });
+
+ it('newError created', () => {
+ expect(AccountStore._newError).to.deep.equal(null);
+ });
+ });
+
+ describe('@action', () => {
+ describe('toggleSelectedAccount', () => {
+ it('Updates the selected accounts', () => {
+ // First set selectedAccounts
+ AccountStore.selectedAccounts = {
+ [ADDRESS]: true,
+ [ADDRESS_2]: false
+ };
+ // Toggle
+ AccountStore.toggleSelectedAccount(ADDRESS_2);
+ // Prep eqality
+ const eq = {
+ [ADDRESS]: true,
+ [ADDRESS_2]: true
+ };
+
+ // Check equality
+ expect(JSON.stringify(AccountStore.selectedAccounts)).to.deep.equal(JSON.stringify(eq));
+ });
+ });
+
+ describe('getPassword', () => {
+ it('Grab from the selected accounts input', () => {
+ // First set passwordInputs
+ AccountStore.passwordInputs = {
+ [ADDRESS]: 'abc'
+ };
+ // getPassword
+ const pass = AccountStore.getPassword(ADDRESS);
+
+ // Check equality
+ expect(AccountStore.passwordInputs[ADDRESS]).to.deep.equal(pass);
+ });
+ });
+
+ describe('setPassword & getPassword', () => {
+ it('First save the input of the selected account, than get the input.', () => {
+ // Set password
+ AccountStore.selectedAccount = ADDRESS;
+ // Set new pass
+ AccountStore.changePassword(null, 'abc');
+ // getPassword
+ const pass = AccountStore.getPassword(ADDRESS);
+
+ // Check equality
+ expect(AccountStore.passwordInputs[ADDRESS]).to.deep.equal(pass);
+ });
+ });
+
+ describe('changePassword', () => {
+ it('Change the stored value with the new input.', () => {
+ // First set selectedAccounts
+ AccountStore.selectedAccounts = {
+ [ADDRESS]: true,
+ [ADDRESS_2]: false
+ };
+ // First set passwordInputs
+ AccountStore.passwordInputs = {
+ [ADDRESS]: 'abc'
+ };
+ // 'Click' on the address:
+ AccountStore.onClick(ADDRESS);
+ // Change password
+ AccountStore.changePassword(null, '123');
+ // Check equality
+ expect(AccountStore.passwordInputs[ADDRESS]).to.deep.equal('123');
+ });
+ });
+ });
+});
diff --git a/js/src/modals/ExportAccount/index.js b/js/src/modals/ExportAccount/index.js
new file mode 100644
index 000000000..eb61a884b
--- /dev/null
+++ b/js/src/modals/ExportAccount/index.js
@@ -0,0 +1,17 @@
+// Copyright 2015-2017 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 .
+
+export default from './exportAccount';
diff --git a/js/src/modals/index.js b/js/src/modals/index.js
index 6fe10bfb1..7c313a2ef 100644
--- a/js/src/modals/index.js
+++ b/js/src/modals/index.js
@@ -24,6 +24,7 @@ export DeleteAccount from './DeleteAccount';
export DeployContract from './DeployContract';
export EditMeta from './EditMeta';
export ExecuteContract from './ExecuteContract';
+export ExportAccount from './ExportAccount';
export Faucet from './Faucet';
export FirstRun from './FirstRun';
export LoadContract from './LoadContract';
diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js
index da59f0e69..de7cc3d18 100644
--- a/js/src/ui/AccountCard/accountCard.js
+++ b/js/src/ui/AccountCard/accountCard.js
@@ -27,6 +27,7 @@ import styles from './accountCard.css';
export default class AccountCard extends Component {
static propTypes = {
+ children: PropTypes.node,
account: PropTypes.object.isRequired,
balance: PropTypes.object,
className: PropTypes.string,
@@ -44,7 +45,7 @@ export default class AccountCard extends Component {
};
render () {
- const { account, balance, className, onFocus } = this.props;
+ const { account, balance, className, onFocus, children } = this.props;
const { copied } = this.state;
const { address, description, meta = {}, name } = account;
const { tags = [] } = meta;
@@ -89,6 +90,7 @@ export default class AccountCard extends Component {
className={ styles.balance }
showOnlyEth
/>
+ { children }
{
diff --git a/js/src/ui/SelectionList/selectionList.js b/js/src/ui/SelectionList/selectionList.js
index a7375581d..266dce893 100644
--- a/js/src/ui/SelectionList/selectionList.js
+++ b/js/src/ui/SelectionList/selectionList.js
@@ -29,7 +29,7 @@ export default class SelectionList extends Component {
items: arrayOrObjectProptype().isRequired,
noStretch: PropTypes.bool,
onDefaultClick: PropTypes.func,
- onSelectClick: PropTypes.func.isRequired,
+ onSelectClick: PropTypes.func,
onSelectDoubleClick: PropTypes.func,
renderItem: PropTypes.func.isRequired
};
@@ -57,8 +57,10 @@ export default class SelectionList extends Component {
: item.checked;
const handleClick = () => {
- onSelectClick(item);
- return false;
+ if (onSelectClick) {
+ onSelectClick(item);
+ return false;
+ }
};
const handleDoubleClick = () => {
onSelectDoubleClick(item);
diff --git a/js/src/views/Account/account.css b/js/src/views/Account/account.css
index 068c07c60..88c5f2a85 100644
--- a/js/src/views/Account/account.css
+++ b/js/src/views/Account/account.css
@@ -19,3 +19,8 @@
width: 24px;
height: 24px;
}
+
+.textbox {
+ line-height: 1.5em;
+ margin-bottom: 1.5em;
+}
diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js
index 6f38cdf72..fdccac793 100644
--- a/js/src/views/Account/account.js
+++ b/js/src/views/Account/account.js
@@ -20,13 +20,15 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
+import { newError } from '~/redux/actions';
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
import HardwareStore from '~/mobx/hardwareStore';
+import ExportStore from '~/modals/ExportAccount/exportStore';
import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
-import { Actionbar, Button, Page } from '~/ui';
-import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
+import { Actionbar, Button, ConfirmDialog, Input, Page, Portal } from '~/ui';
+import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon, FileDownloadIcon } from '~/ui/Icons';
import DeleteAddress from '../Address/Delete';
@@ -42,6 +44,7 @@ class Account extends Component {
};
static propTypes = {
+ accounts: PropTypes.object.isRequired,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
@@ -49,12 +52,20 @@ class Account extends Component {
account: PropTypes.object,
certifications: PropTypes.object,
netVersion: PropTypes.string.isRequired,
+ newError: PropTypes.func,
params: PropTypes.object
}
store = new Store();
hwstore = HardwareStore.get(this.context.api);
+ componentWillMount () {
+ const { accounts, newError, params } = this.props;
+ const { address } = params;
+
+ this.exportStore = new ExportStore(this.context.api, accounts, newError, address);
+ }
+
componentDidMount () {
this.props.fetchCertifiers();
this.setVisibleAccounts();
@@ -63,10 +74,15 @@ class Account extends Component {
componentWillReceiveProps (nextProps) {
const prevAddress = this.props.params.address;
const nextAddress = nextProps.params.address;
+ const { accounts } = nextProps;
if (prevAddress !== nextAddress) {
this.setVisibleAccounts(nextProps);
}
+
+ if (!Object.keys(this.exportStore.accounts).length) {
+ this.exportStore.setAccounts(accounts);
+ }
}
componentWillUnmount () {
@@ -95,6 +111,7 @@ class Account extends Component {