[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:
parent
d24f71f150
commit
1c217cbf2b
@ -120,20 +120,22 @@ export default class HardwareStore {
|
||||
});
|
||||
}
|
||||
|
||||
createAccountInfo (entry) {
|
||||
createAccountInfo (entry, original = {}) {
|
||||
const { address, manufacturer, name } = entry;
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
this._api.parity.setAccountName(address, name),
|
||||
this._api.parity.setAccountMeta(address, {
|
||||
original.name
|
||||
? Promise.resolve(true)
|
||||
: this._api.parity.setAccountName(address, name),
|
||||
this._api.parity.setAccountMeta(address, Object.assign({
|
||||
description: `${manufacturer} ${name}`,
|
||||
hardware: {
|
||||
manufacturer
|
||||
},
|
||||
tags: ['hardware'],
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}, original.meta || {}))
|
||||
])
|
||||
.catch((error) => {
|
||||
console.warn('HardwareStore::createEntry', error);
|
||||
|
@ -130,25 +130,58 @@ describe('mobx/HardwareStore', () => {
|
||||
|
||||
describe('operations', () => {
|
||||
describe('createAccountInfo', () => {
|
||||
beforeEach(() => {
|
||||
return store.createAccountInfo({
|
||||
address: 'testAddr',
|
||||
manufacturer: 'testMfg',
|
||||
name: 'testName'
|
||||
describe('when not existing', () => {
|
||||
beforeEach(() => {
|
||||
return store.createAccountInfo({
|
||||
address: 'testAddr',
|
||||
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', () => {
|
||||
expect(api.parity.setAccountName).to.have.been.calledWith('testAddr', 'testName');
|
||||
});
|
||||
describe('when already exists', () => {
|
||||
beforeEach(() => {
|
||||
return store.createAccountInfo({
|
||||
address: 'testAddr',
|
||||
manufacturer: 'testMfg',
|
||||
name: 'testName'
|
||||
}, {
|
||||
name: 'originalName',
|
||||
meta: {
|
||||
description: 'originalDescription',
|
||||
tags: ['tagA', 'tagB']
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('calls into parity_setAccountMeta', () => {
|
||||
expect(api.parity.setAccountMeta).to.have.been.calledWith('testAddr', sinon.match({
|
||||
description: 'testMfg testName',
|
||||
hardware: {
|
||||
manufacturer: 'testMfg'
|
||||
}
|
||||
}));
|
||||
it('does not call into parity_setAccountName', () => {
|
||||
expect(api.parity.setAccountName).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('calls into parity_setAccountMeta', () => {
|
||||
expect(api.parity.setAccountMeta).to.have.been.calledWith('testAddr', sinon.match({
|
||||
description: 'originalDescription',
|
||||
hardware: {
|
||||
manufacturer: 'testMfg'
|
||||
},
|
||||
tags: ['tagA', 'tagB']
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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 -'
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
@ -70,7 +70,7 @@ export default class FirstRun extends Component {
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='firstRun.welcome.next'
|
||||
|
@ -77,7 +77,8 @@
|
||||
}
|
||||
|
||||
.form {
|
||||
box-sizing: border-box;
|
||||
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);
|
||||
}
|
||||
|
@ -92,6 +92,26 @@ export default class SecureApi extends Api {
|
||||
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 () {
|
||||
if (this._isConnecting) {
|
||||
return;
|
||||
|
@ -68,12 +68,13 @@ $popoverZ: 3600;
|
||||
}
|
||||
|
||||
&.modal {
|
||||
z-index: $modalZ;
|
||||
|
||||
&:not(.small) {
|
||||
bottom: $modalBottom;
|
||||
left: $modalLeft;
|
||||
right: $modalRight;
|
||||
top: $modalTop;
|
||||
z-index: $modalZ;
|
||||
}
|
||||
|
||||
/* TODO: Small Portals don't adjust their overall height like we have with the
|
||||
|
@ -18,6 +18,7 @@ import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactPortal from 'react-portal';
|
||||
import keycode from 'keycode';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
||||
import { CloseIcon } from '~/ui/Icons';
|
||||
@ -29,7 +30,6 @@ import styles from './portal.css';
|
||||
|
||||
export default class Portal extends Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
activeStep: PropTypes.number,
|
||||
busy: PropTypes.bool,
|
||||
@ -45,11 +45,16 @@ export default class Portal extends Component {
|
||||
isChildModal: PropTypes.bool,
|
||||
isSmallModal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
steps: PropTypes.array,
|
||||
title: nodeOrStringProptype()
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onClose: noop
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.setBodyOverflow(this.props.open);
|
||||
}
|
||||
|
@ -394,8 +394,12 @@ class Accounts extends Component {
|
||||
|
||||
Object
|
||||
.keys(wallets)
|
||||
.filter((address) => !accountsInfo[address])
|
||||
.forEach((address) => this.hwstore.createAccountInfo(wallets[address]));
|
||||
.filter((address) => {
|
||||
const account = accountsInfo[address];
|
||||
|
||||
return !account || !account.meta || !account.meta.hardware;
|
||||
})
|
||||
.forEach((address) => this.hwstore.createAccountInfo(wallets[address], accountsInfo[address]));
|
||||
|
||||
this.setVisibleAccounts();
|
||||
}
|
||||
|
122
js/src/views/Accounts/accounts.spec.js
Normal file
122
js/src/views/Accounts/accounts.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -17,12 +17,20 @@
|
||||
import { action, observable } from 'mobx';
|
||||
import store from 'store';
|
||||
|
||||
const OLD_LS_FIRST_RUN_KEY = 'showFirstRun';
|
||||
const LS_FIRST_RUN_KEY = '_parity::showFirstRun';
|
||||
|
||||
export default class Store {
|
||||
@observable firstrunVisible = false;
|
||||
|
||||
constructor (api) {
|
||||
// Migrate the old key to the new one
|
||||
this._migrateStore();
|
||||
|
||||
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();
|
||||
}
|
||||
@ -33,16 +41,41 @@ export default class Store {
|
||||
|
||||
@action toggleFirstrun = (visible = false) => {
|
||||
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 () {
|
||||
this._api.parity
|
||||
.allAccountsInfo()
|
||||
.then((info) => {
|
||||
return Promise
|
||||
.all([
|
||||
this._api.parity.listVaults(),
|
||||
this._api.parity.allAccountsInfo()
|
||||
])
|
||||
.then(([ vaults, info ]) => {
|
||||
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) => {
|
||||
console.error('checkAccounts', error);
|
||||
|
@ -27,7 +27,7 @@ import styles from './transactionPendingFormConfirm.css';
|
||||
|
||||
export default class TransactionPendingFormConfirm extends Component {
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
account: PropTypes.object,
|
||||
address: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
isSending: PropTypes.bool.isRequired,
|
||||
@ -36,6 +36,7 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
account: {},
|
||||
focus: false
|
||||
};
|
||||
|
||||
@ -80,7 +81,7 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
|
||||
getPasswordHint () {
|
||||
const { account } = this.props;
|
||||
const accountHint = account && account.meta && account.meta.passwordHint;
|
||||
const accountHint = account.meta && account.meta.passwordHint;
|
||||
|
||||
if (accountHint) {
|
||||
return accountHint;
|
||||
@ -149,14 +150,16 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
const { account } = this.props;
|
||||
const { password } = this.state;
|
||||
|
||||
if (account && account.hardware) {
|
||||
if (account.hardware) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAccount = account.uuid;
|
||||
|
||||
return (
|
||||
<Input
|
||||
hint={
|
||||
account.uuid
|
||||
isAccount
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.password.unlock.hint'
|
||||
@ -171,7 +174,7 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
)
|
||||
}
|
||||
label={
|
||||
account.uuid
|
||||
isAccount
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.password.unlock.label'
|
||||
|
@ -48,7 +48,7 @@ function render (address) {
|
||||
|
||||
component = shallow(
|
||||
<TransactionPendingFormConfirm
|
||||
account={ ACCOUNTS[address] || {} }
|
||||
account={ ACCOUNTS[address] }
|
||||
address={ address }
|
||||
onConfirm={ onConfirm }
|
||||
isSending={ false }
|
||||
@ -130,5 +130,9 @@ describe('views/Signer/TransactionPendingFormConfirm', () => {
|
||||
it('renders the password', () => {
|
||||
expect(instance.renderPassword()).not.to.be.null;
|
||||
});
|
||||
|
||||
it('renders the hint', () => {
|
||||
expect(instance.renderHint()).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user