Export acc js (#4973)
* Export account RPC * Removing GethDirectory and ParityDirectory * js export accounts as json * js export accounts as json * api - then - catch * final touches * pass * oops * individual accounts * refactoring * refactor one * refactor one * refactor two * some grumble fixes * file name changes * constructor * constructor * git recognize file name change * spec and updates * specs * one tiny fix * one tiny fix * grumbles * more grumbles * sliders * ff * pointer default * grumbles * almost ready * lots of updates * accountList * stupid debuglog * bug fix * bug fix * some more good ol fashioned updates * filter accounts * clean * update spec * ff * ff-f * balances fix
This commit is contained in:
parent
3be3b78c90
commit
cf904b6b2f
177
js/src/modals/ExportAccount/exportAccount.js
Normal file
177
js/src/modals/ExportAccount/exportAccount.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Portal
|
||||||
|
buttons={ [
|
||||||
|
<Button
|
||||||
|
icon={ <CancelIcon /> }
|
||||||
|
key='cancel'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.accounts.button.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={ this.onClose }
|
||||||
|
/>,
|
||||||
|
<Button
|
||||||
|
disabled={ !canExport }
|
||||||
|
icon={ <CheckIcon /> }
|
||||||
|
key='execute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.accounts.button.export'
|
||||||
|
defaultMessage='Export'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={ this.onExport }
|
||||||
|
/>
|
||||||
|
] }
|
||||||
|
onClose={ this.onClose }
|
||||||
|
open
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.accounts.title'
|
||||||
|
defaultMessage='Export an Account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ this.renderList() }
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SelectionList
|
||||||
|
items={ accountList }
|
||||||
|
noStretch
|
||||||
|
onSelectClick={ this.onSelect }
|
||||||
|
renderItem={ this.renderAccount }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAccount = (account) => {
|
||||||
|
const { balances } = this.props;
|
||||||
|
const balance = balances[account.address];
|
||||||
|
const { changePassword, getPassword } = this.exportStore;
|
||||||
|
const password = getPassword(account);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccountCard
|
||||||
|
account={ account }
|
||||||
|
balance={ balance }
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ExportInput
|
||||||
|
account={ account }
|
||||||
|
value={ password }
|
||||||
|
onClick={ this.onClick }
|
||||||
|
onChange={ changePassword }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccountCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
76
js/src/modals/ExportAccount/exportAccount.spec.js
Normal file
76
js/src/modals/ExportAccount/exportAccount.spec.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ExportAccount
|
||||||
|
newError={ NEWERROR }
|
||||||
|
personalAccountsInfo={ PAI }
|
||||||
|
onClose={ ONCLOSE }
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: { api: {}, store: createReduxStore() }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CreateExportModal', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
61
js/src/modals/ExportAccount/exportInput/exportInput.js
Normal file
61
js/src/modals/ExportAccount/exportInput/exportInput.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Input
|
||||||
|
type='password'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.setPassword.label'
|
||||||
|
defaultMessage='Password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.setPassword.hint'
|
||||||
|
defaultMessage='Enter password Here'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={ value }
|
||||||
|
onClick={ this.onClick }
|
||||||
|
onChange={ onChange }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (event) => {
|
||||||
|
const { account, onClick } = this.props;
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
onClick && onClick(account.address);
|
||||||
|
}
|
||||||
|
}
|
17
js/src/modals/ExportAccount/exportInput/index.js
Normal file
17
js/src/modals/ExportAccount/exportInput/index.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export default from './exportInput';
|
106
js/src/modals/ExportAccount/exportStore.js
Normal file
106
js/src/modals/ExportAccount/exportStore.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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})`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
133
js/src/modals/ExportAccount/exportStore.spec.js
Normal file
133
js/src/modals/ExportAccount/exportStore.spec.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
17
js/src/modals/ExportAccount/index.js
Normal file
17
js/src/modals/ExportAccount/index.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export default from './exportAccount';
|
@ -24,6 +24,7 @@ export DeleteAccount from './DeleteAccount';
|
|||||||
export DeployContract from './DeployContract';
|
export DeployContract from './DeployContract';
|
||||||
export EditMeta from './EditMeta';
|
export EditMeta from './EditMeta';
|
||||||
export ExecuteContract from './ExecuteContract';
|
export ExecuteContract from './ExecuteContract';
|
||||||
|
export ExportAccount from './ExportAccount';
|
||||||
export Faucet from './Faucet';
|
export Faucet from './Faucet';
|
||||||
export FirstRun from './FirstRun';
|
export FirstRun from './FirstRun';
|
||||||
export LoadContract from './LoadContract';
|
export LoadContract from './LoadContract';
|
||||||
|
@ -27,6 +27,7 @@ import styles from './accountCard.css';
|
|||||||
|
|
||||||
export default class AccountCard extends Component {
|
export default class AccountCard extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
account: PropTypes.object.isRequired,
|
account: PropTypes.object.isRequired,
|
||||||
balance: PropTypes.object,
|
balance: PropTypes.object,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
@ -44,7 +45,7 @@ export default class AccountCard extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, balance, className, onFocus } = this.props;
|
const { account, balance, className, onFocus, children } = this.props;
|
||||||
const { copied } = this.state;
|
const { copied } = this.state;
|
||||||
const { address, description, meta = {}, name } = account;
|
const { address, description, meta = {}, name } = account;
|
||||||
const { tags = [] } = meta;
|
const { tags = [] } = meta;
|
||||||
@ -89,6 +90,7 @@ export default class AccountCard extends Component {
|
|||||||
className={ styles.balance }
|
className={ styles.balance }
|
||||||
showOnlyEth
|
showOnlyEth
|
||||||
/>
|
/>
|
||||||
|
{ children }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -29,7 +29,7 @@ export default class SelectionList extends Component {
|
|||||||
items: arrayOrObjectProptype().isRequired,
|
items: arrayOrObjectProptype().isRequired,
|
||||||
noStretch: PropTypes.bool,
|
noStretch: PropTypes.bool,
|
||||||
onDefaultClick: PropTypes.func,
|
onDefaultClick: PropTypes.func,
|
||||||
onSelectClick: PropTypes.func.isRequired,
|
onSelectClick: PropTypes.func,
|
||||||
onSelectDoubleClick: PropTypes.func,
|
onSelectDoubleClick: PropTypes.func,
|
||||||
renderItem: PropTypes.func.isRequired
|
renderItem: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
@ -57,8 +57,10 @@ export default class SelectionList extends Component {
|
|||||||
: item.checked;
|
: item.checked;
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
onSelectClick(item);
|
if (onSelectClick) {
|
||||||
return false;
|
onSelectClick(item);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleDoubleClick = () => {
|
const handleDoubleClick = () => {
|
||||||
onSelectDoubleClick(item);
|
onSelectDoubleClick(item);
|
||||||
|
@ -19,3 +19,8 @@
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textbox {
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
@ -20,13 +20,15 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
|
import { newError } from '~/redux/actions';
|
||||||
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
|
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
|
||||||
import HardwareStore from '~/mobx/hardwareStore';
|
import HardwareStore from '~/mobx/hardwareStore';
|
||||||
|
import ExportStore from '~/modals/ExportAccount/exportStore';
|
||||||
import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
|
import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
|
||||||
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
||||||
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
|
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
|
||||||
import { Actionbar, Button, Page } from '~/ui';
|
import { Actionbar, Button, ConfirmDialog, Input, Page, Portal } from '~/ui';
|
||||||
import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
|
import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon, FileDownloadIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
import DeleteAddress from '../Address/Delete';
|
import DeleteAddress from '../Address/Delete';
|
||||||
|
|
||||||
@ -42,6 +44,7 @@ class Account extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
accounts: PropTypes.object.isRequired,
|
||||||
fetchCertifiers: PropTypes.func.isRequired,
|
fetchCertifiers: PropTypes.func.isRequired,
|
||||||
fetchCertifications: PropTypes.func.isRequired,
|
fetchCertifications: PropTypes.func.isRequired,
|
||||||
setVisibleAccounts: PropTypes.func.isRequired,
|
setVisibleAccounts: PropTypes.func.isRequired,
|
||||||
@ -49,12 +52,20 @@ class Account extends Component {
|
|||||||
account: PropTypes.object,
|
account: PropTypes.object,
|
||||||
certifications: PropTypes.object,
|
certifications: PropTypes.object,
|
||||||
netVersion: PropTypes.string.isRequired,
|
netVersion: PropTypes.string.isRequired,
|
||||||
|
newError: PropTypes.func,
|
||||||
params: PropTypes.object
|
params: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
store = new Store();
|
store = new Store();
|
||||||
hwstore = HardwareStore.get(this.context.api);
|
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 () {
|
componentDidMount () {
|
||||||
this.props.fetchCertifiers();
|
this.props.fetchCertifiers();
|
||||||
this.setVisibleAccounts();
|
this.setVisibleAccounts();
|
||||||
@ -63,10 +74,15 @@ class Account extends Component {
|
|||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
const prevAddress = this.props.params.address;
|
const prevAddress = this.props.params.address;
|
||||||
const nextAddress = nextProps.params.address;
|
const nextAddress = nextProps.params.address;
|
||||||
|
const { accounts } = nextProps;
|
||||||
|
|
||||||
if (prevAddress !== nextAddress) {
|
if (prevAddress !== nextAddress) {
|
||||||
this.setVisibleAccounts(nextProps);
|
this.setVisibleAccounts(nextProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(this.exportStore.accounts).length) {
|
||||||
|
this.exportStore.setAccounts(accounts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@ -95,6 +111,7 @@ class Account extends Component {
|
|||||||
<div>
|
<div>
|
||||||
{ this.renderDeleteDialog(account) }
|
{ this.renderDeleteDialog(account) }
|
||||||
{ this.renderEditDialog(account) }
|
{ this.renderEditDialog(account) }
|
||||||
|
{ this.renderExportDialog() }
|
||||||
{ this.renderFaucetDialog() }
|
{ this.renderFaucetDialog() }
|
||||||
{ this.renderFundDialog() }
|
{ this.renderFundDialog() }
|
||||||
{ this.renderPasswordDialog(account) }
|
{ this.renderPasswordDialog(account) }
|
||||||
@ -212,6 +229,17 @@ class Account extends Component {
|
|||||||
}
|
}
|
||||||
onClick={ this.store.toggleEditDialog }
|
onClick={ this.store.toggleEditDialog }
|
||||||
/>,
|
/>,
|
||||||
|
<Button
|
||||||
|
icon={ <FileDownloadIcon /> }
|
||||||
|
key='exportmeta'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.button.export'
|
||||||
|
defaultMessage='export'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={ this.store.toggleExportDialog }
|
||||||
|
/>,
|
||||||
!(account.external || account.hardware) && (
|
!(account.external || account.hardware) && (
|
||||||
<Button
|
<Button
|
||||||
icon={ <LockedIcon /> }
|
icon={ <LockedIcon /> }
|
||||||
@ -320,6 +348,64 @@ class Account extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderExportDialog () {
|
||||||
|
const { changePassword, accountValue } = this.exportStore;
|
||||||
|
|
||||||
|
if (!this.store.isExportVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal
|
||||||
|
open
|
||||||
|
isSmallModal
|
||||||
|
onClose={ this.exportClose }
|
||||||
|
>
|
||||||
|
<ConfirmDialog
|
||||||
|
open
|
||||||
|
disabledConfirm={ false }
|
||||||
|
labelConfirm='Export'
|
||||||
|
labelDeny='Cancel'
|
||||||
|
onConfirm={ this.onExport }
|
||||||
|
onDeny={ this.exportClose }
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.account.title'
|
||||||
|
defaultMessage='Export Account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={ styles.textbox }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.account.info'
|
||||||
|
defaultMessage='Export your account as a JSON file. Please enter the password linked with this account.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className={ styles.textbox }
|
||||||
|
onKeyDown={ this.onEnter }
|
||||||
|
autoFocus
|
||||||
|
type='password'
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.account.password.hint'
|
||||||
|
defaultMessage='The password specified when creating this account'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='export.account.password.label'
|
||||||
|
defaultMessage='Account password'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={ changePassword }
|
||||||
|
value={ accountValue }
|
||||||
|
/>
|
||||||
|
</ConfirmDialog>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderFaucetDialog () {
|
renderFaucetDialog () {
|
||||||
const { netVersion } = this.props;
|
const { netVersion } = this.props;
|
||||||
|
|
||||||
@ -393,6 +479,30 @@ class Account extends Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEnter = (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
this.onExport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportClose = () => {
|
||||||
|
const { toggleExportDialog } = this.store;
|
||||||
|
const { resetAccountValue } = this.exportStore;
|
||||||
|
|
||||||
|
resetAccountValue();
|
||||||
|
toggleExportDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
onExport = () => {
|
||||||
|
const { onExport } = this.exportStore;
|
||||||
|
|
||||||
|
onExport(this.hideExport);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideExport = () => {
|
||||||
|
this.store.toggleExportDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state, props) {
|
function mapStateToProps (state, props) {
|
||||||
@ -406,6 +516,7 @@ function mapStateToProps (state, props) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
account,
|
account,
|
||||||
|
accounts,
|
||||||
certifications,
|
certifications,
|
||||||
netVersion
|
netVersion
|
||||||
};
|
};
|
||||||
@ -415,6 +526,7 @@ function mapDispatchToProps (dispatch) {
|
|||||||
return bindActionCreators({
|
return bindActionCreators({
|
||||||
fetchCertifiers,
|
fetchCertifiers,
|
||||||
fetchCertifications,
|
fetchCertifications,
|
||||||
|
newError,
|
||||||
setVisibleAccounts
|
setVisibleAccounts
|
||||||
}, dispatch);
|
}, dispatch);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import { action, observable } from 'mobx';
|
|||||||
export default class Store {
|
export default class Store {
|
||||||
@observable isDeleteVisible = false;
|
@observable isDeleteVisible = false;
|
||||||
@observable isEditVisible = false;
|
@observable isEditVisible = false;
|
||||||
|
@observable isExportVisible = false;
|
||||||
@observable isFaucetVisible = false;
|
@observable isFaucetVisible = false;
|
||||||
@observable isFundVisible = false;
|
@observable isFundVisible = false;
|
||||||
@observable isPasswordVisible = false;
|
@observable isPasswordVisible = false;
|
||||||
@ -33,6 +34,10 @@ export default class Store {
|
|||||||
this.isEditVisible = !this.isEditVisible;
|
this.isEditVisible = !this.isEditVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action toggleExportDialog = () => {
|
||||||
|
this.isExportVisible = !this.isExportVisible;
|
||||||
|
}
|
||||||
|
|
||||||
@action toggleFaucetDialog = () => {
|
@action toggleFaucetDialog = () => {
|
||||||
this.isFaucetVisible = !this.isFaucetVisible;
|
this.isFaucetVisible = !this.isFaucetVisible;
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,9 @@ import { Link } from 'react-router';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import HardwareStore from '~/mobx/hardwareStore';
|
import HardwareStore from '~/mobx/hardwareStore';
|
||||||
import { CreateAccount, CreateWallet } from '~/modals';
|
import { CreateAccount, CreateWallet, ExportAccount } from '~/modals';
|
||||||
import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '~/ui';
|
import { Actionbar, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '~/ui';
|
||||||
import { AddIcon, KeyIcon } from '~/ui/Icons';
|
import { AddIcon, KeyIcon, FileDownloadIcon } from '~/ui/Icons';
|
||||||
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
||||||
|
|
||||||
import List from './List';
|
import List from './List';
|
||||||
@ -52,6 +52,7 @@ class Accounts extends Component {
|
|||||||
addressBook: false,
|
addressBook: false,
|
||||||
newDialog: false,
|
newDialog: false,
|
||||||
newWalletDialog: false,
|
newWalletDialog: false,
|
||||||
|
newExportDialog: false,
|
||||||
sortOrder: '',
|
sortOrder: '',
|
||||||
searchValues: [],
|
searchValues: [],
|
||||||
searchTokens: [],
|
searchTokens: [],
|
||||||
@ -96,6 +97,7 @@ class Accounts extends Component {
|
|||||||
<div>
|
<div>
|
||||||
{ this.renderNewDialog() }
|
{ this.renderNewDialog() }
|
||||||
{ this.renderNewWalletDialog() }
|
{ this.renderNewWalletDialog() }
|
||||||
|
{ this.renderNewExportDialog() }
|
||||||
{ this.renderActionbar() }
|
{ this.renderActionbar() }
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
@ -244,8 +246,6 @@ class Accounts extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderActionbar () {
|
renderActionbar () {
|
||||||
const { accounts } = this.props;
|
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
<Link
|
<Link
|
||||||
to='/vaults'
|
to='/vaults'
|
||||||
@ -284,10 +284,16 @@ class Accounts extends Component {
|
|||||||
}
|
}
|
||||||
onClick={ this.onNewWalletClick }
|
onClick={ this.onNewWalletClick }
|
||||||
/>,
|
/>,
|
||||||
<ActionbarExport
|
<Button
|
||||||
key='exportAccounts'
|
key='newExport'
|
||||||
content={ accounts }
|
icon={ <FileDownloadIcon /> }
|
||||||
filename='accounts'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='accounts.button.export'
|
||||||
|
defaultMessage='export'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={ this.onNewExportClick }
|
||||||
/>,
|
/>,
|
||||||
this.renderSearchButton(),
|
this.renderSearchButton(),
|
||||||
this.renderSortButton()
|
this.renderSortButton()
|
||||||
@ -351,6 +357,20 @@ class Accounts extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNewExportDialog () {
|
||||||
|
const { newExportDialog } = this.state;
|
||||||
|
|
||||||
|
if (!newExportDialog) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExportAccount
|
||||||
|
onClose={ this.onNewExportClose }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onAddSearchToken = (token) => {
|
onAddSearchToken = (token) => {
|
||||||
const { searchTokens } = this.state;
|
const { searchTokens } = this.state;
|
||||||
const newSearchTokens = uniq([].concat(searchTokens, token));
|
const newSearchTokens = uniq([].concat(searchTokens, token));
|
||||||
@ -370,6 +390,12 @@ class Accounts extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNewExportClick = () => {
|
||||||
|
this.setState({
|
||||||
|
newExportDialog: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onNewAccountClose = () => {
|
onNewAccountClose = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newDialog: false
|
newDialog: false
|
||||||
@ -382,6 +408,12 @@ class Accounts extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNewExportClose = () => {
|
||||||
|
this.setState({
|
||||||
|
newExportDialog: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onNewAccountUpdate = () => {
|
onNewAccountUpdate = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user