[beta] UI backports (#4993)

* [ci skip] js-precompiled 20170314-121823

* Attach hardware wallets already in addressbook (#4912)

* Attach hardware wallets already in addressbook

* Only set values changed

* Add Vaults logic to First Run (#4894) (#4914)

* Add ability to configure Secure API (for #4885) (#4922)

* Add z-index to small modals as well (#4923)

* eth_sign where account === undefined (#4964)

* Update for case where account === undefined

* Update tests to not mask account === undefined

* default account = {} where undefined (thanks @tomusdrw)

* Fix Password Dialog forms style issue (#4968)
This commit is contained in:
Jaco Greeff 2017-03-22 12:45:55 +01:00 committed by Arkadiy Paronyan
parent d24f71f150
commit 1c217cbf2b
12 changed files with 266 additions and 38 deletions

View File

@ -120,20 +120,22 @@ export default class HardwareStore {
}); });
} }
createAccountInfo (entry) { createAccountInfo (entry, original = {}) {
const { address, manufacturer, name } = entry; const { address, manufacturer, name } = entry;
return Promise return Promise
.all([ .all([
this._api.parity.setAccountName(address, name), original.name
this._api.parity.setAccountMeta(address, { ? Promise.resolve(true)
: this._api.parity.setAccountName(address, name),
this._api.parity.setAccountMeta(address, Object.assign({
description: `${manufacturer} ${name}`, description: `${manufacturer} ${name}`,
hardware: { hardware: {
manufacturer manufacturer
}, },
tags: ['hardware'], tags: ['hardware'],
timestamp: Date.now() timestamp: Date.now()
}) }, original.meta || {}))
]) ])
.catch((error) => { .catch((error) => {
console.warn('HardwareStore::createEntry', error); console.warn('HardwareStore::createEntry', error);

View File

@ -130,25 +130,58 @@ describe('mobx/HardwareStore', () => {
describe('operations', () => { describe('operations', () => {
describe('createAccountInfo', () => { describe('createAccountInfo', () => {
beforeEach(() => { describe('when not existing', () => {
return store.createAccountInfo({ beforeEach(() => {
address: 'testAddr', return store.createAccountInfo({
manufacturer: 'testMfg', address: 'testAddr',
name: 'testName' manufacturer: 'testMfg',
name: 'testName'
});
});
it('calls into parity_setAccountName', () => {
expect(api.parity.setAccountName).to.have.been.calledWith('testAddr', 'testName');
});
it('calls into parity_setAccountMeta', () => {
expect(api.parity.setAccountMeta).to.have.been.calledWith('testAddr', sinon.match({
description: 'testMfg testName',
hardware: {
manufacturer: 'testMfg'
},
tags: ['hardware']
}));
}); });
}); });
it('calls into parity_setAccountName', () => { describe('when already exists', () => {
expect(api.parity.setAccountName).to.have.been.calledWith('testAddr', 'testName'); beforeEach(() => {
}); return store.createAccountInfo({
address: 'testAddr',
manufacturer: 'testMfg',
name: 'testName'
}, {
name: 'originalName',
meta: {
description: 'originalDescription',
tags: ['tagA', 'tagB']
}
});
});
it('calls into parity_setAccountMeta', () => { it('does not call into parity_setAccountName', () => {
expect(api.parity.setAccountMeta).to.have.been.calledWith('testAddr', sinon.match({ expect(api.parity.setAccountName).not.to.have.been.called;
description: 'testMfg testName', });
hardware: {
manufacturer: 'testMfg' it('calls into parity_setAccountMeta', () => {
} expect(api.parity.setAccountMeta).to.have.been.calledWith('testAddr', sinon.match({
})); description: 'originalDescription',
hardware: {
manufacturer: 'testMfg'
},
tags: ['tagA', 'tagB']
}));
});
}); });
}); });

View File

@ -49,7 +49,7 @@ export default class FirstRun extends Component {
defaultMessage='As part of a new installation, the next few steps will guide you through the process of setting up you Parity instance and your associated accounts. Our aim is to make it as simple as possible and to get you up and running in record-time, so please bear with us. Once completed you will have -' defaultMessage='As part of a new installation, the next few steps will guide you through the process of setting up you Parity instance and your associated accounts. Our aim is to make it as simple as possible and to get you up and running in record-time, so please bear with us. Once completed you will have -'
/> />
</p> </p>
<p> <div>
<ul> <ul>
<li> <li>
<FormattedMessage <FormattedMessage
@ -70,7 +70,7 @@ export default class FirstRun extends Component {
/> />
</li> </li>
</ul> </ul>
</p> </div>
<p> <p>
<FormattedMessage <FormattedMessage
id='firstRun.welcome.next' id='firstRun.welcome.next'

View File

@ -77,7 +77,8 @@
} }
.form { .form {
box-sizing: border-box;
margin-top: 0; margin-top: 0;
padding: 0.75rem 1.5rem 1.5rem 1.5rem; padding: 0.75rem 1.5rem 1.5rem;
background-color: rgba(255, 255, 255, 0.05); background-color: rgba(255, 255, 255, 0.05);
} }

View File

@ -92,6 +92,26 @@ export default class SecureApi extends Api {
return this._transport.token; return this._transport.token;
} }
/**
* Configure the current API with the given values
* (`signerPort`, `dappsInterface`, `dappsPort`, ...)
*/
configure (configuration) {
const { dappsInterface, dappsPort, signerPort } = configuration;
if (dappsInterface) {
this._dappsInterface = dappsInterface;
}
if (dappsPort) {
this._dappsPort = dappsPort;
}
if (signerPort) {
this._signerPort = signerPort;
}
}
connect () { connect () {
if (this._isConnecting) { if (this._isConnecting) {
return; return;

View File

@ -68,12 +68,13 @@ $popoverZ: 3600;
} }
&.modal { &.modal {
z-index: $modalZ;
&:not(.small) { &:not(.small) {
bottom: $modalBottom; bottom: $modalBottom;
left: $modalLeft; left: $modalLeft;
right: $modalRight; right: $modalRight;
top: $modalTop; top: $modalTop;
z-index: $modalZ;
} }
/* TODO: Small Portals don't adjust their overall height like we have with the /* TODO: Small Portals don't adjust their overall height like we have with the

View File

@ -18,6 +18,7 @@ import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import ReactPortal from 'react-portal'; import ReactPortal from 'react-portal';
import keycode from 'keycode'; import keycode from 'keycode';
import { noop } from 'lodash';
import { nodeOrStringProptype } from '~/util/proptypes'; import { nodeOrStringProptype } from '~/util/proptypes';
import { CloseIcon } from '~/ui/Icons'; import { CloseIcon } from '~/ui/Icons';
@ -29,7 +30,6 @@ import styles from './portal.css';
export default class Portal extends Component { export default class Portal extends Component {
static propTypes = { static propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired, open: PropTypes.bool.isRequired,
activeStep: PropTypes.number, activeStep: PropTypes.number,
busy: PropTypes.bool, busy: PropTypes.bool,
@ -45,11 +45,16 @@ export default class Portal extends Component {
isChildModal: PropTypes.bool, isChildModal: PropTypes.bool,
isSmallModal: PropTypes.bool, isSmallModal: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
onClose: PropTypes.func,
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
steps: PropTypes.array, steps: PropTypes.array,
title: nodeOrStringProptype() title: nodeOrStringProptype()
}; };
static defaultProps = {
onClose: noop
};
componentDidMount () { componentDidMount () {
this.setBodyOverflow(this.props.open); this.setBodyOverflow(this.props.open);
} }

View File

@ -394,8 +394,12 @@ class Accounts extends Component {
Object Object
.keys(wallets) .keys(wallets)
.filter((address) => !accountsInfo[address]) .filter((address) => {
.forEach((address) => this.hwstore.createAccountInfo(wallets[address])); const account = accountsInfo[address];
return !account || !account.meta || !account.meta.hardware;
})
.forEach((address) => this.hwstore.createAccountInfo(wallets[address], accountsInfo[address]));
this.setVisibleAccounts(); this.setVisibleAccounts();
} }

View File

@ -0,0 +1,122 @@
// 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 Accounts from './';
let api;
let component;
let hwstore;
let instance;
let redux;
function createApi () {
api = {};
return api;
}
function createHwStore (walletAddress = '0x456') {
hwstore = {
wallets: {
[walletAddress]: {
address: walletAddress
}
},
createAccountInfo: sinon.stub()
};
return hwstore;
}
function createRedux () {
redux = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accounts: {},
accountsInfo: {
'0x123': { meta: '1' },
'0x999': { meta: { hardware: {} } }
}
},
balances: {
balances: {}
}
};
}
};
return redux;
}
function render (props = {}) {
component = shallow(
<Accounts { ...props } />,
{
context: {
store: createRedux()
}
}
).find('Accounts').shallow({
context: {
api: createApi()
}
});
instance = component.instance();
return component;
}
describe('views/Accounts', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('instance event methods', () => {
describe('onHardwareChange', () => {
it('detects completely new entries', () => {
instance.hwstore = createHwStore();
instance.onHardwareChange();
expect(hwstore.createAccountInfo).to.have.been.calledWith({ address: '0x456' });
});
it('detects addressbook entries', () => {
instance.hwstore = createHwStore('0x123');
instance.onHardwareChange();
expect(hwstore.createAccountInfo).to.have.been.calledWith({ address: '0x123' }, { meta: '1' });
});
it('ignores existing hardware entries', () => {
instance.hwstore = createHwStore('0x999');
instance.onHardwareChange();
expect(hwstore.createAccountInfo).not.to.have.been.called;
});
});
});
});

View File

@ -17,12 +17,20 @@
import { action, observable } from 'mobx'; import { action, observable } from 'mobx';
import store from 'store'; import store from 'store';
const OLD_LS_FIRST_RUN_KEY = 'showFirstRun';
const LS_FIRST_RUN_KEY = '_parity::showFirstRun';
export default class Store { export default class Store {
@observable firstrunVisible = false; @observable firstrunVisible = false;
constructor (api) { constructor (api) {
// Migrate the old key to the new one
this._migrateStore();
this._api = api; this._api = api;
this.firstrunVisible = store.get('showFirstRun'); // Show the first run if it hasn't been shown before
// (thus an undefined value)
this.firstrunVisible = store.get(LS_FIRST_RUN_KEY) === undefined;
this._checkAccounts(); this._checkAccounts();
} }
@ -33,16 +41,41 @@ export default class Store {
@action toggleFirstrun = (visible = false) => { @action toggleFirstrun = (visible = false) => {
this.firstrunVisible = visible; this.firstrunVisible = visible;
store.set('showFirstRun', !!visible);
// There's no need to write to storage that the
// First Run should be visible
if (!visible) {
store.set(LS_FIRST_RUN_KEY, !!visible);
}
}
/**
* Migrate the old LocalStorage ket format
* to the new one
*/
_migrateStore () {
const oldValue = store.get(OLD_LS_FIRST_RUN_KEY);
const newValue = store.get(LS_FIRST_RUN_KEY);
if (newValue === undefined && oldValue !== undefined) {
store.set(LS_FIRST_RUN_KEY, oldValue);
store.remove(OLD_LS_FIRST_RUN_KEY);
}
} }
_checkAccounts () { _checkAccounts () {
this._api.parity return Promise
.allAccountsInfo() .all([
.then((info) => { this._api.parity.listVaults(),
this._api.parity.allAccountsInfo()
])
.then(([ vaults, info ]) => {
const accounts = Object.keys(info).filter((address) => info[address].uuid); const accounts = Object.keys(info).filter((address) => info[address].uuid);
// Has accounts if any vaults or accounts
const hasAccounts = (accounts && accounts.length > 0) || (vaults && vaults.length > 0);
this.toggleFirstrun(this.firstrunVisible || !accounts || !accounts.length); // Show First Run if no accounts and no vaults
this.toggleFirstrun(this.firstrunVisible || !hasAccounts);
}) })
.catch((error) => { .catch((error) => {
console.error('checkAccounts', error); console.error('checkAccounts', error);

View File

@ -27,7 +27,7 @@ import styles from './transactionPendingFormConfirm.css';
export default class TransactionPendingFormConfirm extends Component { export default class TransactionPendingFormConfirm extends Component {
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
disabled: PropTypes.bool, disabled: PropTypes.bool,
isSending: PropTypes.bool.isRequired, isSending: PropTypes.bool.isRequired,
@ -36,6 +36,7 @@ export default class TransactionPendingFormConfirm extends Component {
}; };
static defaultProps = { static defaultProps = {
account: {},
focus: false focus: false
}; };
@ -80,7 +81,7 @@ export default class TransactionPendingFormConfirm extends Component {
getPasswordHint () { getPasswordHint () {
const { account } = this.props; const { account } = this.props;
const accountHint = account && account.meta && account.meta.passwordHint; const accountHint = account.meta && account.meta.passwordHint;
if (accountHint) { if (accountHint) {
return accountHint; return accountHint;
@ -149,14 +150,16 @@ export default class TransactionPendingFormConfirm extends Component {
const { account } = this.props; const { account } = this.props;
const { password } = this.state; const { password } = this.state;
if (account && account.hardware) { if (account.hardware) {
return null; return null;
} }
const isAccount = account.uuid;
return ( return (
<Input <Input
hint={ hint={
account.uuid isAccount
? ( ? (
<FormattedMessage <FormattedMessage
id='signer.txPendingConfirm.password.unlock.hint' id='signer.txPendingConfirm.password.unlock.hint'
@ -171,7 +174,7 @@ export default class TransactionPendingFormConfirm extends Component {
) )
} }
label={ label={
account.uuid isAccount
? ( ? (
<FormattedMessage <FormattedMessage
id='signer.txPendingConfirm.password.unlock.label' id='signer.txPendingConfirm.password.unlock.label'

View File

@ -48,7 +48,7 @@ function render (address) {
component = shallow( component = shallow(
<TransactionPendingFormConfirm <TransactionPendingFormConfirm
account={ ACCOUNTS[address] || {} } account={ ACCOUNTS[address] }
address={ address } address={ address }
onConfirm={ onConfirm } onConfirm={ onConfirm }
isSending={ false } isSending={ false }
@ -130,5 +130,9 @@ describe('views/Signer/TransactionPendingFormConfirm', () => {
it('renders the password', () => { it('renders the password', () => {
expect(instance.renderPassword()).not.to.be.null; expect(instance.renderPassword()).not.to.be.null;
}); });
it('renders the hint', () => {
expect(instance.renderHint()).to.be.null;
});
}); });
}); });