Store for EditPassword Modal (#3979)

* External store (WIP)

* address & meta

* Add editable (WIP)

* View converted (WIP)

* Single API stub creation

* Testing (WIP)

* Simplified meta assign

* Tests running

* Fix duplicate exports

* Fix tags not editable
This commit is contained in:
Jaco Greeff 2016-12-28 18:09:45 +01:00 committed by Gav Wood
parent 3067a8de3e
commit 7e600b5a82
13 changed files with 658 additions and 304 deletions

View File

@ -85,7 +85,7 @@ export default class EditMeta extends Component {
defaultMessage='(optional) tags' /> defaultMessage='(optional) tags' />
} }
onTokensChange={ this.store.setTags } onTokensChange={ this.store.setTags }
tokens={ tags } /> tokens={ tags.slice() } />
</Form> </Form>
</Modal> </Modal>
); );

View File

@ -20,7 +20,7 @@ import sinon from 'sinon';
import EditMeta from './'; import EditMeta from './';
import { ACCOUNT } from './editMeta.test.js'; import { ACCOUNT, createApi } from './editMeta.test.js';
let component; let component;
let onClose; let onClose;
@ -35,12 +35,7 @@ function render (props) {
onClose={ onClose } />, onClose={ onClose } />,
{ {
context: { context: {
api: { api: createApi()
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
}
} }
} }
); );

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ACCOUNT = { const ACCOUNT = {
address: '0x123456789a123456789a123456789a123456789a', address: '0x123456789a123456789a123456789a123456789a',
meta: { meta: {
@ -39,7 +41,17 @@ const ADDRESS = {
name: 'Random address' name: 'Random address'
}; };
function createApi () {
return {
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
};
}
export { export {
ACCOUNT, ACCOUNT,
ADDRESS ADDRESS,
createApi
}; };

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, computed, observable, toJS, transaction } from 'mobx'; import { action, computed, observable, transaction } from 'mobx';
import { newError } from '~/redux/actions'; import { newError } from '~/redux/actions';
import { validateName } from '~/util/validation'; import { validateName } from '~/util/validation';
@ -23,25 +23,27 @@ export default class Store {
@observable address = null; @observable address = null;
@observable isAccount = false; @observable isAccount = false;
@observable description = null; @observable description = null;
@observable meta = {}; @observable meta = null;
@observable name = null; @observable name = null;
@observable nameError = null; @observable nameError = null;
@observable passwordHint = null; @observable passwordHint = null;
@observable tags = []; @observable tags = null;
constructor (api, account) { constructor (api, account) {
const { address, name, meta, uuid } = account; const { address, name, meta, uuid } = account;
this._api = api; this._api = api;
this.isAccount = !!uuid; transaction(() => {
this.address = address; this.isAccount = !!uuid;
this.meta = Object.assign({}, meta || {}); this.address = address;
this.name = name || ''; this.meta = meta || {};
this.name = name || '';
this.description = this.meta.description || ''; this.description = this.meta.description || '';
this.passwordHint = this.meta.passwordHint || ''; this.passwordHint = this.meta.passwordHint || '';
this.tags = [].concat((meta || {}).tags || []); this.tags = this.meta.tags && this.meta.tags.peek() || [];
});
} }
@computed get hasError () { @computed get hasError () {
@ -70,7 +72,7 @@ export default class Store {
} }
@action setTags = (tags) => { @action setTags = (tags) => {
this.tags = [].concat(tags); this.tags = tags.slice();
} }
save () { save () {
@ -86,7 +88,7 @@ export default class Store {
return Promise return Promise
.all([ .all([
this._api.parity.setAccountName(this.address, this.name), this._api.parity.setAccountName(this.address, this.name),
this._api.parity.setAccountMeta(this.address, Object.assign({}, toJS(this.meta), meta)) this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta))
]) ])
.catch((error) => { .catch((error) => {
console.error('onSave', error); console.error('onSave', error);

View File

@ -14,22 +14,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { toJS } from 'mobx';
import sinon from 'sinon';
import Store from './store'; import Store from './store';
import { ACCOUNT, ADDRESS } from './editMeta.test.js'; import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js';
let api; let api;
let store; let store;
function createStore (account) { function createStore (account) {
api = { api = createApi();
parity: {
setAccountName: sinon.stub().resolves(),
setAccountMeta: sinon.stub().resolves()
}
};
store = new Store(api, account); store = new Store(api, account);
@ -56,12 +48,12 @@ describe('modals/EditMeta/Store', () => {
}); });
it('extracts the tags', () => { it('extracts the tags', () => {
expect(store.tags.peek()).to.deep.equal(ACCOUNT.meta.tags); expect(store.tags).to.deep.equal(ACCOUNT.meta.tags);
}); });
describe('meta', () => { describe('meta', () => {
it('extracts the full meta', () => { it('extracts the full meta', () => {
expect(toJS(store.meta)).to.deep.equal(ACCOUNT.meta); expect(store.meta).to.deep.equal(ACCOUNT.meta);
}); });
it('extracts the description', () => { it('extracts the description', () => {

View File

@ -14,54 +14,55 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ContentClear from 'material-ui/svg-icons/content/clear';
import CheckIcon from 'material-ui/svg-icons/navigation/check';
import SendIcon from 'material-ui/svg-icons/content/send';
import { Tabs, Tab } from 'material-ui/Tabs';
import Paper from 'material-ui/Paper'; import Paper from 'material-ui/Paper';
import { Tabs, Tab } from 'material-ui/Tabs';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { newError, showSnackbar } from '~/redux/actions';
import { bindActionCreators } from 'redux';
import { showSnackbar } from '~/redux/providers/snackbarActions';
import Form, { Input } from '~/ui/Form';
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui'; import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
import Form, { Input } from '~/ui/Form';
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
import Store, { CHANGE_ACTION, TEST_ACTION } from './store';
import styles from './passwordManager.css'; import styles from './passwordManager.css';
const TEST_ACTION = 'TEST_ACTION'; const MSG_SUCCESS_STYLE = {
const CHANGE_ACTION = 'CHANGE_ACTION'; backgroundColor: 'rgba(174, 213, 129, 0.75)'
};
const MSG_FAILURE_STYLE = {
backgroundColor: 'rgba(229, 115, 115, 0.75)'
};
const TABS_INKBAR_STYLE = {
backgroundColor: 'rgba(255, 255, 255, 0.55)'
};
const TABS_ITEM_STYLE = {
backgroundColor: 'rgba(255, 255, 255, 0.05)'
};
class PasswordManager extends Component { @observer
export default class PasswordManager extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
showSnackbar: PropTypes.func.isRequired,
onClose: PropTypes.func onClose: PropTypes.func
} }
state = { store = new Store(this.context.api, this.props.account);
action: TEST_ACTION,
waiting: false,
showMessage: false,
message: { value: '', success: true },
currentPass: '',
newPass: '',
repeatNewPass: '',
repeatValid: true,
passwordHint: this.props.account.meta && this.props.account.meta.passwordHint || ''
}
render () { render () {
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
title='Password Manager' title={
<FormattedMessage
id='passwordChange.title'
defaultMessage='Password Manager' />
}
visible> visible>
{ this.renderAccount() } { this.renderAccount() }
{ this.renderPage() } { this.renderPage() }
@ -71,150 +72,168 @@ class PasswordManager extends Component {
} }
renderMessage () { renderMessage () {
const { message, showMessage } = this.state; const { infoMessage } = this.store;
const style = message.success if (!infoMessage) {
? { return null;
backgroundColor: 'rgba(174, 213, 129, 0.75)'
}
: {
backgroundColor: 'rgba(229, 115, 115, 0.75)'
};
const classes = [ styles.message ];
if (!showMessage) {
classes.push(styles.hideMessage);
} }
return ( return (
<Paper <Paper
zDepth={ 1 } className={ `${styles.message}` }
style={ style } style={
className={ classes.join(' ') }> infoMessage.success
{ message.value } ? MSG_SUCCESS_STYLE
: MSG_FAILURE_STYLE
}
zDepth={ 1 }>
{ infoMessage.value }
</Paper> </Paper>
); );
} }
renderAccount () { renderAccount () {
const { account } = this.props; const { address, passwordHint } = this.store;
const { address, meta } = account;
const passwordHint = meta && meta.passwordHint
? (
<span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ meta.passwordHint }
</span>
)
: null;
return ( return (
<div className={ styles.accountContainer }> <div className={ styles.accountContainer }>
<IdentityIcon <IdentityIcon address={ address } />
address={ address }
/>
<div className={ styles.accountInfos }> <div className={ styles.accountInfos }>
<IdentityName <IdentityName
className={ styles.accountName }
address={ address } address={ address }
unknown className={ styles.accountName }
/> unknown />
<span className={ styles.accountAddress }> <span className={ styles.accountAddress }>
{ address } { address }
</span> </span>
{ passwordHint } <span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ passwordHint || '-' }
</span>
</div> </div>
</div> </div>
); );
} }
renderPage () { renderPage () {
const { account } = this.props; const { busy, isRepeatValid, passwordHint } = this.store;
const { waiting, repeatValid } = this.state;
const disabled = !!waiting;
const repeatError = repeatValid
? null
: 'the two passwords differ';
const { meta } = account;
const passwordHint = meta && meta.passwordHint || '';
return ( return (
<Tabs <Tabs
inkBarStyle={ { inkBarStyle={ TABS_INKBAR_STYLE }
backgroundColor: 'rgba(255, 255, 255, 0.55)' tabItemContainerStyle={ TABS_ITEM_STYLE }>
} }
tabItemContainerStyle={ {
backgroundColor: 'rgba(255, 255, 255, 0.05)'
} }
>
<Tab <Tab
onActive={ this.handleTestActive } label={
label='Test Password' <FormattedMessage
> id='passwordChange.tabTest.label'
<Form defaultMessage='Test Password' />
className={ styles.form } }
> onActive={ this.onActivateTestTab }>
<Form className={ styles.form }>
<div> <div>
<Input <Input
label='password' disabled={ busy }
hint='your current password for this account' hint={
type='password' <FormattedMessage
id='passwordChange.testPassword.hint'
defaultMessage='your account password' />
}
label={
<FormattedMessage
id='passwordChange.testPassword.label'
defaultMessage='password' />
}
onChange={ this.onEditTestPassword }
onSubmit={ this.testPassword }
submitOnBlur={ false } submitOnBlur={ false }
disabled={ disabled } type='password' />
onSubmit={ this.handleTestPassword }
onChange={ this.onEditCurrent } />
</div> </div>
</Form> </Form>
</Tab> </Tab>
<Tab <Tab
onActive={ this.handleChangeActive } label={
label='Change Password' <FormattedMessage
> id='passwordChange.tabChange.label'
<Form defaultMessage='Change Password' />
className={ styles.form } }
> onActive={ this.onActivateChangeTab }>
<Form className={ styles.form }>
<div> <div>
<Input <Input
label='current password' disabled={ busy }
hint='your current password for this account' hint={
type='password' <FormattedMessage
id='passwordChange.currentPassword.hint'
defaultMessage='your current password for this account' />
}
label={
<FormattedMessage
id='passwordChange.currentPassword.label'
defaultMessage='current password' />
}
onChange={ this.onEditCurrentPassword }
onSubmit={ this.changePassword }
submitOnBlur={ false } submitOnBlur={ false }
disabled={ disabled } type='password' />
onSubmit={ this.handleChangePassword }
onChange={ this.onEditCurrent } />
<Input <Input
label='(optional) new password hint' disabled={ busy }
hint='hint for the new password' hint={
<FormattedMessage
id='passwordChange.passwordHint.hint'
defaultMessage='hint for the new password' />
}
label={
<FormattedMessage
id='passwordChange.passwordHint.label'
defaultMessage='(optional) new password hint' />
}
onChange={ this.onEditNewPasswordHint }
onSubmit={ this.changePassword }
submitOnBlur={ false } submitOnBlur={ false }
value={ passwordHint } value={ passwordHint } />
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditHint } />
<div className={ styles.passwords }> <div className={ styles.passwords }>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
label='new password' disabled={ busy }
hint='the new password for this account' hint={
type='password' <FormattedMessage
id='passwordChange.newPassword.hint'
defaultMessage='the new password for this account' />
}
label={
<FormattedMessage
id='passwordChange.newPassword.label'
defaultMessage='new password' />
}
onChange={ this.onEditNewPassword }
onSubmit={ this.changePassword }
submitOnBlur={ false } submitOnBlur={ false }
disabled={ disabled } type='password' />
onSubmit={ this.handleChangePassword }
onChange={ this.onEditNew } />
</div> </div>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
label='repeat new password' disabled={ busy }
hint='repeat the new password for this account' error={
type='password' isRepeatValid
? null
: <FormattedMessage
id='passwordChange.repeatPassword.error'
defaultMessage='the supplied passwords do not match' />
}
hint={
<FormattedMessage
id='passwordChange.repeatPassword.hint'
defaultMessage='repeat the new password for this account' />
}
label={
<FormattedMessage
id='passwordChange.repeatPassword.label'
defaultMessage='repeat new password' />
}
onChange={ this.onEditNewPasswordRepeat }
onSubmit={ this.changePassword }
submitOnBlur={ false } submitOnBlur={ false }
error={ repeatError } type='password' />
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditRepeatNew } />
</div> </div>
</div> </div>
</div> </div>
@ -225,176 +244,118 @@ class PasswordManager extends Component {
} }
renderDialogActions () { renderDialogActions () {
const { actionTab, busy, isRepeatValid } = this.store;
const { onClose } = this.props; const { onClose } = this.props;
const { action, waiting, repeatValid } = this.state;
const cancelBtn = ( const cancelBtn = (
<Button <Button
icon={ <ContentClear /> } icon={ <CancelIcon /> }
label='Cancel' key='cancel'
label={
<FormattedMessage
id='passwordChange.button.cancel'
defaultMessage='Cancel' />
}
onClick={ onClose } /> onClick={ onClose } />
); );
if (waiting) { if (busy) {
const waitingBtn = ( return [
cancelBtn,
<Button <Button
disabled disabled
label='Wait...' /> key='wait'
); label={
<FormattedMessage
return [ cancelBtn, waitingBtn ]; id='passwordChange.button.wait'
defaultMessage='Wait...' />
} />
];
} }
if (action === TEST_ACTION) { if (actionTab === TEST_ACTION) {
const testBtn = ( return [
cancelBtn,
<Button <Button
icon={ <CheckIcon /> } icon={ <CheckIcon /> }
label='Test' key='test'
onClick={ this.handleTestPassword } /> label={
); <FormattedMessage
id='passwordChange.button.test'
return [ cancelBtn, testBtn ]; defaultMessage='Test' />
}
onClick={ this.testPassword } />
];
} }
const changeBtn = ( return [
cancelBtn,
<Button <Button
disabled={ !repeatValid } disabled={ !isRepeatValid }
icon={ <SendIcon /> } icon={ <SendIcon /> }
label='Change' key='change'
onClick={ this.handleChangePassword } /> label={
); <FormattedMessage
id='passwordChange.button.change'
return [ cancelBtn, changeBtn ]; defaultMessage='Change' />
}
onEditCurrent = (event, value) => {
this.setState({
currentPass: value,
showMessage: false
});
}
onEditNew = (event, value) => {
const repeatValid = value === this.state.repeatNewPass;
this.setState({
newPass: value,
showMessage: false,
repeatValid
});
}
onEditRepeatNew = (event, value) => {
const repeatValid = value === this.state.newPass;
this.setState({
repeatNewPass: value,
showMessage: false,
repeatValid
});
}
onEditHint = (event, value) => {
this.setState({
passwordHint: value,
showMessage: false
});
}
handleTestActive = () => {
this.setState({
action: TEST_ACTION,
showMessage: false
});
}
handleChangeActive = () => {
this.setState({
action: CHANGE_ACTION,
showMessage: false
});
}
handleTestPassword = () => {
const { account } = this.props;
const { currentPass } = this.state;
this.setState({ waiting: true, showMessage: false });
this.context
.api.parity
.testPassword(account.address, currentPass)
.then(correct => {
const message = correct
? { value: 'This password is correct', success: true }
: { value: 'This password is not correct', success: false };
this.setState({ waiting: false, message, showMessage: true });
})
.catch(e => {
console.error('passwordManager::handleTestPassword', e);
this.setState({ waiting: false });
});
}
handleChangePassword = () => {
const { account, showSnackbar, onClose } = this.props;
const { currentPass, newPass, repeatNewPass, passwordHint } = this.state;
if (repeatNewPass !== newPass) {
return;
}
this.setState({ waiting: true, showMessage: false });
this.context
.api.parity
.testPassword(account.address, currentPass)
.then(correct => {
if (!correct) {
const message = {
value: 'This provided current password is not correct',
success: false
};
this.setState({ waiting: false, message, showMessage: true });
return false;
} }
onClick={ this.changePassword } />
];
}
const meta = Object.assign({}, account.meta, { onActivateChangeTab = () => {
passwordHint this.store.setActionTab(CHANGE_ACTION);
}); }
return Promise.all([ onActivateTestTab = () => {
this.context this.store.setActionTab(TEST_ACTION);
.api.parity }
.setAccountMeta(account.address, meta),
this.context onEditCurrentPassword = (event, password) => {
.api.parity this.store.setPassword(password);
.changePassword(account.address, currentPass, newPass) }
])
.then(() => { onEditNewPassword = (event, password) => {
showSnackbar(<div>Your password has been successfully changed.</div>); this.store.setNewPassword(password);
this.setState({ waiting: false, showMessage: false }); }
onClose();
}); onEditNewPasswordHint = (event, passwordHint) => {
this.store.setNewPasswordHint(passwordHint);
}
onEditNewPasswordRepeat = (event, password) => {
this.store.setNewPasswordRepeat(password);
}
onEditTestPassword = (event, password) => {
this.store.setValidatePassword(password);
}
changePassword = () => {
return this.store
.changePassword()
.then((result) => {
if (result) {
showSnackbar(
<div>
<FormattedMessage
id='passwordChange.success'
defaultMessage='Your password has been successfully changed' />
</div>
);
this.props.onClose();
}
}) })
.catch(e => { .catch((error) => {
console.error('passwordManager::handleChangePassword', e); newError(error);
this.setState({ waiting: false }); });
}
testPassword = () => {
return this.store
.testPassword()
.catch((error) => {
newError(error);
}); });
} }
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
showSnackbar
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(PasswordManager);

View File

@ -0,0 +1,81 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import PasswordManager from './';
import { ACCOUNT, createApi } from './passwordManager.test.js';
let component;
let onClose;
function render (props) {
onClose = sinon.stub();
component = shallow(
<PasswordManager
{ ...props }
account={ ACCOUNT }
onClose={ onClose } />,
{
context: {
api: createApi()
}
}
);
return component;
}
describe('modals/PasswordManager', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});
describe('actions', () => {
beforeEach(() => {
render();
});
describe('changePassword', () => {
it('calls store.changePassword & props.onClose', () => {
const instance = component.instance();
sinon.spy(instance.store, 'changePassword');
instance.changePassword().then(() => {
expect(instance.store.changePassword).to.have.been.called;
expect(onClose).to.have.been.called;
});
});
});
describe('testPassword', () => {
it('calls store.testPassword', () => {
const instance = component.instance();
sinon.spy(instance.store, 'testPassword');
instance.testPassword().then(() => {
expect(instance.store.testPassword).to.have.been.called;
});
});
});
});
});

View File

@ -0,0 +1,43 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ACCOUNT = {
address: '0x123456789a123456789a123456789a123456789a',
meta: {
description: 'Call me bob',
passwordHint: 'some hint',
tags: ['testing']
},
name: 'Bobby',
uuid: '123-456'
};
function createApi (result = true) {
return {
parity: {
changePassword: sinon.stub().resolves(result),
setAccountMeta: sinon.stub().resolves(result),
testPassword: sinon.stub().resolves(result)
}
};
}
export {
ACCOUNT,
createApi
};

View File

@ -0,0 +1,161 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, computed, observable, transaction } from 'mobx';
const CHANGE_ACTION = 'CHANGE_ACTION';
const TEST_ACTION = 'TEST_ACTION';
export default class Store {
@observable actionTab = TEST_ACTION;
@observable address = null;
@observable busy = false;
@observable infoMessage = null;
@observable meta = null;
@observable newPassword = '';
@observable newPasswordHint = '';
@observable newPasswordRepeat = '';
@observable password = '';
@observable passwordHint = '';
@observable validatePassword = '';
constructor (api, account) {
this._api = api;
this.address = account.address;
this.meta = account.meta || {};
this.passwordHint = this.meta.passwordHint || '';
}
@computed get isRepeatValid () {
return this.newPasswordRepeat === this.newPassword;
}
@action setActionTab = (actionTab) => {
transaction(() => {
this.actionTab = actionTab;
this.setInfoMessage();
});
}
@action setBusy = (busy, message) => {
transaction(() => {
this.busy = busy;
this.setInfoMessage(message);
});
}
@action setInfoMessage = (message = null) => {
this.infoMessage = message;
}
@action setPassword = (password) => {
transaction(() => {
this.password = password;
this.setInfoMessage();
});
}
@action setNewPassword = (password) => {
transaction(() => {
this.newPassword = password;
this.setInfoMessage();
});
}
@action setNewPasswordHint = (passwordHint) => {
transaction(() => {
this.newPasswordHint = passwordHint;
this.setInfoMessage();
});
}
@action setNewPasswordRepeat = (password) => {
transaction(() => {
this.newPasswordRepeat = password;
this.setInfoMessage();
});
}
@action setValidatePassword = (password) => {
transaction(() => {
this.validatePassword = password;
this.setInfoMessage();
});
}
changePassword = () => {
if (!this.isRepeatValid) {
return Promise.resolve(false);
}
this.setBusy(true);
return this
.testPassword(this.password)
.then((result) => {
if (!result) {
return false;
}
const meta = Object.assign({}, this.meta, {
passwordHint: this.newPasswordHint
});
return Promise
.all([
this._api.parity.setAccountMeta(this.address, meta),
this._api.parity.changePassword(this.address, this.password, this.newPassword)
])
.then(() => {
this.setBusy(false);
return true;
});
})
.catch((error) => {
console.error('changePassword', error);
this.setBusy(false);
throw error;
});
}
testPassword = (password) => {
this.setBusy(false);
return this._api.parity
.testPassword(this.address, password || this.validatePassword)
.then((success) => {
this.setBusy(false, {
success,
value: success
? 'This password is correct'
: 'This password is not correct'
});
return success;
})
.catch((error) => {
console.error('testPassword', error);
this.setBusy(false);
throw error;
});
}
}
export {
CHANGE_ACTION,
TEST_ACTION
};

View File

@ -0,0 +1,103 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Store from './store';
import { ACCOUNT, createApi } from './passwordManager.test.js';
let api;
let store;
function createStore (account) {
api = createApi();
store = new Store(api, account);
return store;
}
describe('modals/PasswordManager/Store', () => {
beforeEach(() => {
createStore(ACCOUNT);
});
describe('constructor', () => {
it('extracts the address', () => {
expect(store.address).to.equal(ACCOUNT.address);
});
describe('meta', () => {
it('extracts the full meta', () => {
expect(store.meta).to.deep.equal(ACCOUNT.meta);
});
it('extracts the passwordHint', () => {
expect(store.passwordHint).to.equal(ACCOUNT.meta.passwordHint);
});
});
});
describe('operations', () => {
const CUR_PASSWORD = 'aPassW0rd';
const NEW_PASSWORD = 'br@ndNEW';
const NEW_HINT = 'something new to test';
describe('changePassword', () => {
beforeEach(() => {
store.setPassword(CUR_PASSWORD);
store.setNewPasswordHint(NEW_HINT);
store.setNewPassword(NEW_PASSWORD);
store.setNewPasswordRepeat(NEW_PASSWORD);
});
it('calls parity.testPassword with current password', () => {
return store.changePassword().then(() => {
expect(api.parity.testPassword).to.have.been.calledWith(ACCOUNT.address, CUR_PASSWORD);
});
});
it('calls parity.setAccountMeta with new hint', () => {
return store.changePassword().then(() => {
expect(api.parity.setAccountMeta).to.have.been.calledWith(ACCOUNT.address, Object.assign({}, ACCOUNT.meta, {
passwordHint: NEW_HINT
}));
});
});
it('calls parity.changePassword with the new password', () => {
return store.changePassword().then(() => {
expect(api.parity.changePassword).to.have.been.calledWith(ACCOUNT.address, CUR_PASSWORD, NEW_PASSWORD);
});
});
});
describe('testPassword', () => {
beforeEach(() => {
store.setValidatePassword(CUR_PASSWORD);
});
it('calls parity.testPassword', () => {
return store.testPassword().then(() => {
expect(api.parity.testPassword).to.have.been.calledWith(ACCOUNT.address, CUR_PASSWORD);
});
});
it('sets the infoMessage for success/failure', () => {
return store.testPassword().then(() => {
expect(store.infoMessage).not.to.be.null;
});
});
});
});
});

View File

@ -16,6 +16,7 @@
import { newError } from '~/ui/Errors/actions'; import { newError } from '~/ui/Errors/actions';
import { setAddressImage } from './providers/imagesActions'; import { setAddressImage } from './providers/imagesActions';
import { showSnackbar } from './providers/snackbarActions';
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions'; import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
import { toggleView } from '~/views/Settings/actions'; import { toggleView } from '~/views/Settings/actions';
@ -23,6 +24,7 @@ export {
newError, newError,
clearStatusLogs, clearStatusLogs,
setAddressImage, setAddressImage,
showSnackbar,
toggleStatusLogs, toggleStatusLogs,
toggleStatusRefresh, toggleStatusRefresh,
toggleView toggleView

View File

@ -24,6 +24,7 @@ import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save'; import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye'; import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
@ -38,6 +39,7 @@ export {
NextIcon, NextIcon,
PrevIcon, PrevIcon,
SaveIcon, SaveIcon,
SendIcon,
SnoozeIcon, SnoozeIcon,
VisibleIcon VisibleIcon
}; };

View File

@ -72,7 +72,7 @@ export default class Header extends Component {
</div> </div>
<div className={ styles.tags }> <div className={ styles.tags }>
<Tags tags={ meta.tags } /> <Tags tags={ meta.tags.slice() } />
</div> </div>
<div className={ styles.balances }> <div className={ styles.balances }>
<Balance <Balance