diff --git a/js/src/mobx/hardwareStore.js b/js/src/mobx/hardwareStore.js index 65213ad4e..46bf3fa58 100644 --- a/js/src/mobx/hardwareStore.js +++ b/js/src/mobx/hardwareStore.js @@ -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); diff --git a/js/src/mobx/hardwareStore.spec.js b/js/src/mobx/hardwareStore.spec.js index 14feb5740..784fc3f10 100644 --- a/js/src/mobx/hardwareStore.spec.js +++ b/js/src/mobx/hardwareStore.spec.js @@ -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'] + })); + }); }); }); diff --git a/js/src/modals/FirstRun/Welcome/welcome.js b/js/src/modals/FirstRun/Welcome/welcome.js index 629ec47bc..df438d112 100644 --- a/js/src/modals/FirstRun/Welcome/welcome.js +++ b/js/src/modals/FirstRun/Welcome/welcome.js @@ -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 -' />

-

+

-

+

!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(); } diff --git a/js/src/views/Accounts/accounts.spec.js b/js/src/views/Accounts/accounts.spec.js new file mode 100644 index 000000000..7bd798105 --- /dev/null +++ b/js/src/views/Accounts/accounts.spec.js @@ -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 . + +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( + , + { + 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; + }); + }); + }); +}); diff --git a/js/src/views/Application/store.js b/js/src/views/Application/store.js index 4f3b3f451..8d2093d45 100644 --- a/js/src/views/Application/store.js +++ b/js/src/views/Application/store.js @@ -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); diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 13bf27876..8a20b3332 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -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 ( { it('renders the password', () => { expect(instance.renderPassword()).not.to.be.null; }); + + it('renders the hint', () => { + expect(instance.renderHint()).to.be.null; + }); }); });