diff --git a/js/src/views/ParityBar/accountStore.js b/js/src/views/ParityBar/accountStore.js new file mode 100644 index 000000000..b53f40dd2 --- /dev/null +++ b/js/src/views/ParityBar/accountStore.js @@ -0,0 +1,101 @@ +// 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 { action, observable, transaction } from 'mobx'; + +export default class AccountStore { + @observable accounts = []; + @observable defaultAccount = null; + @observable isLoading = false; + + constructor (api) { + this._api = api; + + this.loadAccounts(); + this.subscribeDefaultAccount(); + } + + @action setAccounts = (accounts) => { + this.accounts = accounts; + } + + @action setDefaultAccount = (defaultAccount) => { + this.defaultAccount = defaultAccount; + } + + @action setLoading = (isLoading) => { + this.isLoading = isLoading; + } + + makeDefaultAccount = (address) => { + const accounts = [address].concat( + this.accounts + .filter((account) => account.address !== address) + .map((account) => account.address) + ); + + return this._api.parity + .setNewDappsWhitelist(accounts) + .catch((error) => { + console.warn('makeDefaultAccount', error); + }); + } + + loadAccounts () { + this.setLoading(true); + + return Promise + .all([ + this._api.parity.getNewDappsWhitelist(), + this._api.parity.allAccountsInfo() + ]) + .then(([whitelist, accounts]) => { + transaction(() => { + this.setLoading(false); + this.setAccounts( + Object + .keys(accounts) + .filter((address) => { + const isAccount = accounts[address].uuid; + const isWhitelisted = !whitelist || whitelist.includes(address); + + return isAccount && isWhitelisted; + }) + .map((address) => { + const account = accounts[address]; + + account.address = address; + account.default = address === this.defaultAccount; + + return account; + }) + ); + }); + }) + .catch((error) => { + this.setLoading(false); + console.warn('loadAccounts', error); + }); + } + + subscribeDefaultAccount () { + return this._api.subscribe('parity_defaultAccount', (error, defaultAccount) => { + if (!error) { + this.setDefaultAccount(defaultAccount); + } + }); + } +} diff --git a/js/src/views/ParityBar/accountStore.spec.js b/js/src/views/ParityBar/accountStore.spec.js new file mode 100644 index 000000000..6dd219806 --- /dev/null +++ b/js/src/views/ParityBar/accountStore.spec.js @@ -0,0 +1,104 @@ +// 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 sinon from 'sinon'; + +import AccountStore from './accountStore'; + +import { ACCOUNT_DEFAULT, ACCOUNT_FIRST, ACCOUNT_NEW, createApi } from './parityBar.test.js'; + +let api; +let store; + +function create () { + api = createApi(); + store = new AccountStore(api); + + return store; +} + +describe('views/ParityBar/AccountStore', () => { + beforeEach(() => { + create(); + }); + + describe('constructor', () => { + it('subscribes to defaultAccount', () => { + expect(api.subscribe).to.have.been.calledWith('parity_defaultAccount'); + }); + }); + + describe('@action', () => { + describe('setAccounts', () => { + it('sets the accounts', () => { + store.setAccounts('testing'); + expect(store.accounts).to.equal('testing'); + }); + }); + + describe('setDefaultAccount', () => { + it('sets the default account', () => { + store.setDefaultAccount('testing'); + expect(store.defaultAccount).to.equal('testing'); + }); + }); + + describe('setLoading', () => { + it('sets the loading status', () => { + store.setLoading('testing'); + expect(store.isLoading).to.equal('testing'); + }); + }); + }); + + describe('operations', () => { + describe('loadAccounts', () => { + beforeEach(() => { + sinon.spy(store, 'setAccounts'); + + return store.loadAccounts(); + }); + + afterEach(() => { + store.setAccounts.restore(); + }); + + it('calls into parity_getNewDappsWhitelist', () => { + expect(api.parity.getNewDappsWhitelist).to.have.been.called; + }); + + it('calls into parity_allAccountsInfo', () => { + expect(api.parity.allAccountsInfo).to.have.been.called; + }); + + it('sets the accounts', () => { + expect(store.setAccounts).to.have.been.called; + }); + }); + + describe('makeDefaultAccount', () => { + beforeEach(() => { + return store.makeDefaultAccount(ACCOUNT_NEW); + }); + + it('calls into parity_setNewDappsWhitelist (with ordering)', () => { + expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith([ + ACCOUNT_NEW, ACCOUNT_FIRST, ACCOUNT_DEFAULT + ]); + }); + }); + }); +}); diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css index 7fe283e69..1d37cb177 100644 --- a/js/src/views/ParityBar/parityBar.css +++ b/js/src/views/ParityBar/parityBar.css @@ -15,6 +15,44 @@ /* along with Parity. If not, see . */ +.account { + display: flex; + flex: 1; + position: relative; + + .accountOverlay { + position: absolute; + right: 0.5em; + top: 0.5em; + } + + .iconDisabled { + opacity: 0.15; + } + + .selected, + .unselected { + margin: 0.125em 0; + + &:focus { + outline: none; + } + } + + .unselected { + background: rgba(0, 0, 0, 0.4) !important; + } + + .selected { + background: rgba(255, 255, 255, 0.35) !important; + } +} + +.container { + display: flex; + flex-direction: column; +} + .overlay { position: fixed; top: 0; @@ -26,7 +64,8 @@ user-select: none; } -.bar, .expanded { +.bar, +.expanded { position: fixed; font-size: 16px; font-family: 'Roboto', sans-serif; @@ -110,7 +149,9 @@ margin-left: 1em; } -.button, .parityButton { +.button, +.iconButton, +.parityButton { overflow: visible !important; } @@ -123,6 +164,14 @@ fill: white !important; } +.iconButton { + min-width: 2em !important; + + img { + margin: 6px 0.5em 0 0.5em; + } +} + .label { position: relative; display: inline-block; @@ -151,7 +200,8 @@ } } -.header, .corner { +.header, +.corner { button { color: white !important; } @@ -180,7 +230,8 @@ } } -.parityIcon, .signerIcon { +.parityIcon, +.signerIcon { width: 24px; height: 24px; vertical-align: middle; diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index f4711589f..63036b9e4 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -14,25 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { throttle } from 'lodash'; +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import { connect } from 'react-redux'; -import { throttle } from 'lodash'; import store from 'store'; import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg'; +import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SectionList } from '~/ui'; import { CancelIcon, FingerprintIcon } from '~/ui/Icons'; -import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui'; -import { Embedded as Signer } from '../Signer'; import DappsStore from '~/views/Dapps/dappsStore'; +import { Embedded as Signer } from '~/views/Signer'; +import AccountStore from './accountStore'; import styles from './parityBar.css'; const LS_STORE_KEY = '_parity::parityBar'; const DEFAULT_POSITION = { right: '1em', bottom: 0 }; +const DISPLAY_ACCOUNTS = 'accounts'; +const DISPLAY_SIGNER = 'signer'; +@observer class ParityBar extends Component { app = null; measures = null; @@ -49,6 +54,7 @@ class ParityBar extends Component { }; state = { + displayType: DISPLAY_SIGNER, moving: false, opened: false, position: DEFAULT_POSITION @@ -67,6 +73,8 @@ class ParityBar extends Component { componentWillMount () { const { api } = this.context; + this.accountStore = new AccountStore(api); + // Hook to the dapp loaded event to position the // Parity Bar accordingly DappsStore.get(api).on('loaded', (app) => { @@ -84,14 +92,14 @@ class ParityBar extends Component { } if (count < newCount) { - this.setOpened(true); + this.setOpened(true, DISPLAY_SIGNER); } else if (newCount === 0 && count === 1) { this.setOpened(false); } } - setOpened (opened) { - this.setState({ opened }); + setOpened (opened, displayType = DISPLAY_SIGNER) { + this.setState({ displayType, opened }); if (!this.bar) { return; @@ -186,9 +194,19 @@ class ParityBar extends Component { } return ( -
+
+
@@ -267,27 +285,85 @@ class ParityBar extends Component { } renderExpanded () { + const { displayType } = this.state; + return ( -
+
- + + ) + : ( + + ) + } + />
- + { + displayType === DISPLAY_ACCOUNTS + ? ( + + ) + : ( + + ) + }
); } + renderAccount = (account) => { + const onMakeDefault = () => { + this.toggleAccountsDisplay(); + this.accountStore.makeDefaultAccount(account.address); + }; + + return ( +
+ +
+ ); + } + renderLabel (name, bubble) { return (
@@ -497,10 +573,20 @@ class ParityBar extends Component { this.savePosition(position); } - toggleDisplay = () => { + toggleAccountsDisplay = () => { const { opened } = this.state; - this.setOpened(!opened); + this.setOpened(!opened, DISPLAY_ACCOUNTS); + + if (!opened) { + this.accountStore.loadAccounts(); + } + } + + toggleSignerDisplay = () => { + const { opened } = this.state; + + this.setOpened(!opened, DISPLAY_SIGNER); } get config () { @@ -542,13 +628,22 @@ class ParityBar extends Component { stringToPosition (value) { switch (value) { case 'top-left': - return { top: 0, left: '1em' }; + return { + left: '1em', + top: 0 + }; case 'top-right': - return { top: 0, right: '1em' }; + return { + right: '1em', + top: 0 + }; case 'bottom-left': - return { bottom: 0, left: '1em' }; + return { + bottom: 0, + left: '1em' + }; case 'bottom-right': default: diff --git a/js/src/views/ParityBar/parityBar.spec.js b/js/src/views/ParityBar/parityBar.spec.js index 194e27656..941c47c65 100644 --- a/js/src/views/ParityBar/parityBar.spec.js +++ b/js/src/views/ParityBar/parityBar.spec.js @@ -20,6 +20,9 @@ import sinon from 'sinon'; import ParityBar from './'; +import { createApi } from './parityBar.test.js'; + +let api; let component; let instance; let store; @@ -35,6 +38,7 @@ function createRedux (state = {}) { } function render (props = {}, state = {}) { + api = createApi(); component = shallow( , { @@ -42,7 +46,7 @@ function render (props = {}, state = {}) { store: createRedux(state) } } - ).find('ParityBar').shallow({ context: { api: {} } }); + ).find('ParityBar').shallow({ context: { api } }); instance = component.instance(); return component; @@ -77,8 +81,14 @@ describe('views/ParityBar', () => { expect(bar.find('div')).not.to.have.length(0); }); + it('renders the Account selector button', () => { + const icon = bar.find('Button').first().props().icon; + + expect(icon.type.displayName).to.equal('Connect(IdentityIcon)'); + }); + it('renders the Parity button', () => { - const label = shallow(bar.find('Button').first().props().label); + const label = shallow(bar.find('Button').at(1).props().label); expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.parity'); }); diff --git a/js/src/views/ParityBar/parityBar.test.js b/js/src/views/ParityBar/parityBar.test.js new file mode 100644 index 000000000..2623e4074 --- /dev/null +++ b/js/src/views/ParityBar/parityBar.test.js @@ -0,0 +1,55 @@ +// 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 sinon from 'sinon'; + +const ACCOUNT_DEFAULT = '0x2345678901'; +const ACCOUNT_FIRST = '0x1234567890'; +const ACCOUNT_NEW = '0x0987654321'; +const ACCOUNTS = { + [ACCOUNT_FIRST]: { uuid: 123 }, + [ACCOUNT_DEFAULT]: { uuid: 234 }, + '0x3456789012': {}, + [ACCOUNT_NEW]: { uuid: 456 } +}; + +function createApi () { + const api = { + subscribe: (params, callback) => { + callback(null, ACCOUNT_DEFAULT); + + return Promise.resolve(1); + }, + parity: { + defaultAccount: sinon.stub().resolves(ACCOUNT_DEFAULT), + allAccountsInfo: sinon.stub().resolves(ACCOUNTS), + getNewDappsWhitelist: sinon.stub().resolves(null), + setNewDappsWhitelist: sinon.stub().resolves(true) + } + }; + + sinon.spy(api, 'subscribe'); + + return api; +} + +export { + ACCOUNT_DEFAULT, + ACCOUNT_FIRST, + ACCOUNT_NEW, + ACCOUNTS, + createApi +};