diff options
36 files changed, 1151 insertions, 737 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a607c19a..0f0151e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,20 @@ ## Current Master +- Replace account screen with an account drop-down menu. +- Replace confusing buttons with a new account-specific drop-down menu. + +## 3.9.5 2017-8-04 + +- Improved phishing detection configuration update rate + +## 3.9.4 2017-8-03 + +- Fixed bug that prevented transactions from being rejected. + ## 3.9.3 2017-8-03 -- Add support for EGO uport token +- Add support for EGO ujo token - Continuously update blacklist for known phishing sites in background. - Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. @@ -78,7 +89,7 @@ ## 3.7.8 2017-6-12 -- Add a `ethereum:` prefix to the QR code address +- Add an `ethereum:` prefix to the QR code address - The default network on installation is now MainNet - Fix currency API URL from cryptonator. - Update gasLimit params with every new block seen. @@ -234,7 +245,7 @@ - Add ability to import accounts in JSON file format (used by Mist, Geth, MyEtherWallet, and more!) - Fix unapproved messages not being included in extension badge. -- Fix rendering bug where the Confirm transaction view would lets you approve transactions when the account has insufficient balance. +- Fix rendering bug where the Confirm transaction view would let you approve transactions when the account has insufficient balance. ## 3.1.2 2017-1-24 @@ -257,8 +268,8 @@ ## 3.0.0 2017-1-16 - Fix seed word account generation (https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.t4i1qmmsz). -- Fix Bug where you see a empty transaction flash by on the confirm transaction view. -- Create visible difference in transaction history between a approved but not yet included in a block transaction and a transaction who has been confirmed. +- Fix Bug where you see an empty transaction flash by on the confirm transaction view. +- Create visible difference in transaction history between an approved but not yet included in a block transaction and a transaction who has been confirmed. - Fix memory leak in RPC Cache - Override RPC commands eth_syncing and web3_clientVersion - Remove certain non-essential permissions from certain builds. @@ -313,7 +324,7 @@ - Fix bug where gas estimate would sometimes be very high. - Increased our gas estimate from 100k gas to 20% of estimate. -- Fix github link on info page to point at current repository. +- Fix GitHub link on info page to point at current repository. ## 2.13.6 2016-10-26 @@ -389,7 +400,7 @@ popup notification opens up. - Block negative values from transactions. - Fixed a memory leak. - MetaMask logo now renders as super lightweight SVG, improving compatibility and performance. -- Now showing loading indication during vault unlocking, to clarify behavior for users who are experience slow unlocks. +- Now showing loading indication during vault unlocking, to clarify behavior for users who are experiencing slow unlocks. - Now only initially creates one wallet when restoring a vault, to reduce some users' confusion. ## 2.10.2 2016-09-02 @@ -421,7 +432,7 @@ popup notification opens up. - Added info link on account screen that visits Etherscan. - Fixed bug where a message signing request would be lost if the vault was locked. - Added shortcut to open MetaMask (Ctrl+Alt+M or Cmd+Opt/Alt+M) -- Prevent API calls in tests. +- Prevent API calls in tests. - Fixed bug where sign message confirmation would sometimes render blank. ## 2.9.0 2016-08-22 diff --git a/app/home.html b/app/home.html new file mode 100644 index 000000000..cfb4b00a0 --- /dev/null +++ b/app/home.html @@ -0,0 +1,12 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no"> + <title>MetaMask Plugin</title> + </head> + <body> + <div id="app-content"></div> + <script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script> + </body> +</html> diff --git a/app/manifest.json b/app/manifest.json index 6c02120c1..3b9194eb9 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.3", + "version": "3.9.5", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/app/popup.html b/app/popup.html index 6d85a9811..d09b09315 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,10 +2,11 @@ <html> <head> <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no"> <title>MetaMask Plugin</title> </head> - <body> + <body style="width:357px; height:500px;"> <div id="app-content"></div> <script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script> </body> -</html>
\ No newline at end of file +</html> diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index 7e01fa386..dd671943f 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -4,8 +4,8 @@ const PhishingDetector = require('eth-phishing-detect/src/detector') // compute phishing lists const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') -// every ten minutes -const POLLING_INTERVAL = 10 * 60 * 1000 +// every four minutes +const POLLING_INTERVAL = 4 * 60 * 1000 class BlacklistController { @@ -41,6 +41,7 @@ class BlacklistController { scheduleUpdates () { if (this._phishingUpdateIntervalRef) return + this.updatePhishingList() this._phishingUpdateIntervalRef = setInterval(() => { this.updatePhishingList() }, POLLING_INTERVAL) diff --git a/package.json b/package.json index 3b2e714db..a275247ba 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "eth-bin-to-ops": "^1.0.1", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", - "eth-phishing-detect": "^1.1.0", + "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", @@ -92,7 +92,6 @@ "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "loglevel": "^1.4.1", - "menu-droppo": "^1.1.0", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", @@ -110,7 +109,7 @@ "pumpify": "^1.3.4", "qrcode-npm": "0.0.3", "react": "^15.0.2", - "react-addons-css-transition-group": "^15.0.2", + "react-addons-css-transition-group": "^15.6.0", "react-dom": "^15.5.4", "react-hyperscript": "^2.2.2", "react-markdown": "^2.3.0", diff --git a/test/integration/index.js b/test/integration/index.js index 85f91d92b..e089fc39b 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -1,23 +1,19 @@ -var fs = require('fs') -var path = require('path') -var browserify = require('browserify') -var tests = fs.readdirSync(path.join(__dirname, 'lib')) -var bundlePath = path.join(__dirname, 'bundle.js') +const fs = require('fs') +const path = require('path') +const browserify = require('browserify') +const tests = fs.readdirSync(path.join(__dirname, 'lib')) +const bundlePath = path.join(__dirname, 'bundle.js') -var b = browserify() +const b = browserify() -// Remove old bundle -try { - fs.unlinkSync(bundlePath) +const writeStream = fs.createWriteStream(bundlePath) - var writeStream = fs.createWriteStream(bundlePath) - - tests.forEach(function (fileName) { - b.add(path.join(__dirname, 'lib', fileName)) - }) - - b.bundle().pipe(writeStream) -} catch (e) { - console.error('Integration build failure', e) -} +tests.forEach(function (fileName) { + b.add(path.join(__dirname, 'lib', fileName)) +}) +b.bundle() +.pipe(writeStream) +.on('error', (err) => { + throw err +}) diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index 6c8cedbac..0e4b802da 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -90,7 +90,13 @@ QUnit.test('render init screen', function (assert) { return wait() }).then(function (){ - var qrButton = app.find('.fa.fa-qrcode')[0] + var qrButton = app.find('.fa.fa-ellipsis-h')[0] // open account settings dropdown + qrButton.click() + + return wait(1000) + }).then(function (){ + + var qrButton = app.find('.dropdown-menu-item')[1] // qr code item qrButton.click() return wait(1000) diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js new file mode 100644 index 000000000..3ad2c390e --- /dev/null +++ b/test/unit/responsive/components/dropdown-test.js @@ -0,0 +1,115 @@ +var assert = require('assert'); + +const additions = require('react-testutils-additions'); +const h = require('react-hyperscript'); +const ReactTestUtils = require('react-addons-test-utils'); +const sinon = require('sinon'); +const path = require('path'); +const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdown.js')).Dropdown; +const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdown.js')).DropdownMenuItem; + +describe('Dropdown components', function () { + let onClickOutside; + let closeMenu; + let onClick; + + let dropdownComponentProps; + const renderer = ReactTestUtils.createRenderer() + beforeEach(function () { + onClickOutside = sinon.spy(); + closeMenu = sinon.spy(); + onClick = sinon.spy(); + + dropdownComponentProps = { + isOpen: true, + zIndex: 11, + onClickOutside, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: {}, + } + }); + + it('can render two items', function () { + const dropdownComponent = h( + Dropdown, + dropdownComponentProps, + [ + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ] + ) + + const component = additions.renderIntoDocument(dropdownComponent); + renderer.render(dropdownComponent); + const items = additions.find(component, 'li'); + assert.equal(items.length, 2); + }); + + it('closes when item clicked', function() { + const dropdownComponent = h( + Dropdown, + dropdownComponentProps, + [ + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ] + ) + const component = additions.renderIntoDocument(dropdownComponent); + renderer.render(dropdownComponent); + const items = additions.find(component, 'li'); + const node = items[0]; + ReactTestUtils.Simulate.click(node); + assert.equal(closeMenu.calledOnce, true); + }); + + it('invokes click handler when item clicked', function() { + const dropdownComponent = h( + Dropdown, + dropdownComponentProps, + [ + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ] + ) + const component = additions.renderIntoDocument(dropdownComponent); + renderer.render(dropdownComponent); + const items = additions.find(component, 'li'); + const node = items[0]; + ReactTestUtils.Simulate.click(node); + assert.equal(onClick.calledOnce, true); + }); +}); diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index bed05a7fb..22a883096 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -3,21 +3,17 @@ const extend = require('xtend') const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const valuesFor = require('./util').valuesFor - const Identicon = require('./components/identicon') const EthBalance = require('./components/eth-balance') const TransactionList = require('./components/transaction-list') const ExportAccountView = require('./components/account-export') const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') const TabBar = require('./components/tab-bar') const TokenList = require('./components/token-list') +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -54,12 +50,13 @@ AccountDetailScreen.prototype.render = function () { return ( - h('.account-detail-section', [ + h('.account-detail-section.full-flex-height', [ - // identicon, label, balance, etc + // identicon, label, balance, etc h('.account-data-subsection', { style: { margin: '0 20px', + flex: '1 0 auto', }, }, [ @@ -84,6 +81,7 @@ AccountDetailScreen.prototype.render = function () { style: { lineHeight: '10px', marginLeft: '15px', + width: '100%', }, }, [ h(EditableLabel, { @@ -98,7 +96,43 @@ AccountDetailScreen.prototype.render = function () { // What is shown when not editing + edit text: h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + h( + 'div', + { + style: { + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + [ + h( + 'h2.font-medium.color-forest', + { + name: 'edit', + style: { + }, + }, + [ + identity && identity.name, + ] + ), + h( + AccountDropdowns, + { + style: { + marginRight: '8px', + marginLeft: 'auto', + cursor: 'pointer', + }, + selected, + network, + identities: props.identities, + enableAccountOptions: true, + }, + ), + ] + ), ]), h('.flex-row', { style: { @@ -119,61 +153,10 @@ AccountDetailScreen.prototype.render = function () { fontSize: '13px', fontFamily: 'Montserrat Light', textRendering: 'geometricPrecision', - marginTop: '10px', marginBottom: '15px', color: '#AEAEAE', }, }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), ]), // account ballence @@ -197,14 +180,11 @@ AccountDetailScreen.prototype.render = function () { }, }), + h('.flex-grow'), + h('button', { onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, + style: { marginRight: '10px' }, }, 'BUY'), h('button', { @@ -219,14 +199,7 @@ AccountDetailScreen.prototype.render = function () { ]), // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), + this.subview(), ]) ) @@ -254,7 +227,7 @@ AccountDetailScreen.prototype.subview = function () { AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props - return h('section.tabSection', [ + return h('section.tabSection.full-flex-height.grow-tenx', [ h(TabBar, { tabs: [ @@ -305,7 +278,3 @@ AccountDetailScreen.prototype.transactionList = function () { }, }) } - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') - -const EthBalance = require('../components/eth-balance') -const CopyButton = require('../components/copyButton') -const Identicon = require('../components/identicon') - -module.exports = AccountListItem - -inherits(AccountListItem, Component) -function AccountListItem () { - Component.call(this) -} - -AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail, - conversionRate, currentCurrency } = this.props - - const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) - const isSelected = selectedAddress === identity.address - const account = accounts[identity.address] - const selectedClass = isSelected ? '.selected' : '' - - return ( - h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { - key: `account-panel-${identity.address}`, - onClick: (event) => onShowDetail(identity.address, event), - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - this.pendingOrNot(), - this.indicateIfLoose(), - h(Identicon, { - address: identity.address, - imageify: true, - }), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { - style: { - width: '200px', - }, - }, [ - h('span', identity.name), - h('span.font-small', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, checksumAddress), - h(EthBalance, { - value: account && account.balance, - currentCurrency, - conversionRate, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - ]), - - // copy button - h('.identity-copy.flex-column', { - style: { - margin: '0 20px', - }, - }, [ - h(CopyButton, { - value: checksumAddress, - }), - ]), - ]) - ) -} - -AccountListItem.prototype.indicateIfLoose = function () { - try { // Sometimes keyrings aren't loaded yet: - const type = this.props.keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label', 'LOOSE') : null - } catch (e) { return } -} - -AccountListItem.prototype.pendingOrNot = function () { - const pending = this.props.pending - if (pending.length === 0) return null - return h('.pending-dot', pending.length) -} diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js deleted file mode 100644 index ac2615cd7..000000000 --- a/ui/app/accounts/index.js +++ /dev/null @@ -1,164 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../actions') -const valuesFor = require('../util').valuesFor -const findDOMNode = require('react-dom').findDOMNode -const AccountListItem = require('./account-list-item') - -module.exports = connect(mapStateToProps)(AccountsScreen) - -function mapStateToProps (state) { - const pendingTxs = valuesFor(state.metamask.unapprovedTxs) - .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) - const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) - const pending = pendingTxs.concat(pendingMsgs) - - return { - accounts: state.metamask.accounts, - identities: state.metamask.identities, - unapprovedTxs: state.metamask.unapprovedTxs, - selectedAddress: state.metamask.selectedAddress, - scrollToBottom: state.appState.scrollToBottom, - pending, - keyrings: state.metamask.keyrings, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(AccountsScreen, Component) -function AccountsScreen () { - Component.call(this) -} - -AccountsScreen.prototype.render = function () { - const props = this.props - const { keyrings, conversionRate, currentCurrency } = props - const identityList = valuesFor(props.identities) - const unapprovedTxList = valuesFor(props.unapprovedTxs) - - return ( - - h('.accounts-section.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }), - h('h2.page-subtitle', 'Select Account'), - ]), - - h('hr.horizontal-line'), - - // identity selection - h('section.identity-section', { - style: { - height: '418px', - overflowY: 'auto', - overflowX: 'hidden', - }, - }, - [ - identityList.map((identity) => { - const pending = this.props.pending.filter((txOrMsg) => { - if ('txParams' in txOrMsg) { - return txOrMsg.txParams.from === identity.address - } else if ('msgParams' in txOrMsg) { - return txOrMsg.msgParams.from === identity.address - } else { - return false - } - }) - - const simpleAddress = identity.address.substring(2).toLowerCase() - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h(AccountListItem, { - key: `acct-panel-${identity.address}`, - identity, - selectedAddress: this.props.selectedAddress, - conversionRate, - currentCurrency, - accounts: this.props.accounts, - onShowDetail: this.onShowDetail.bind(this), - pending, - keyring, - }) - }), - - h('hr.horizontal-line'), - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.addNewAccount() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg', {key: ''}), - ]), - h('hr.horizontal-line'), - ]), - - unapprovedTxList.length ? ( - - h('.unconftx-link.flex-row.flex-center', { - onClick: this.navigateToConfTx.bind(this), - }, [ - h('span', 'Unconfirmed Txs'), - h('i.fa.fa-arrow-right.fa-lg'), - ]) - - ) : ( - null - ), - ]) - ) -} - -// If a new account was revealed, scroll to the bottom -AccountsScreen.prototype.componentDidUpdate = function () { - const scrollToBottom = this.props.scrollToBottom - - if (scrollToBottom) { - var container = findDOMNode(this) - var scrollable = container.querySelector('.identity-section') - scrollable.scrollTop = scrollable.scrollHeight - } -} - -AccountsScreen.prototype.navigateToConfTx = function () { - event.stopPropagation() - this.props.dispatch(actions.showConfTxPage()) -} - -AccountsScreen.prototype.onShowDetail = function (address, event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountDetail(address)) -} - -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.addNewAccount(0)) -} - -/* An optional view proposed in this design: - * https://consensys.quip.com/zZVrAysM5znY -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.navigateToNewAccountScreen()) -} -*/ - -AccountsScreen.prototype.goHome = function () { - this.props.dispatch(actions.goHome()) -} diff --git a/ui/app/actions.js b/ui/app/actions.js index 8ff8bbbdd..eafd04b4c 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -709,7 +709,7 @@ function markAccountsFound () { // // default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { +function setDefaultRpcTarget () { log.debug(`background.setDefaultRpcTarget`) return (dispatch) => { background.setDefaultRpc((err, result) => { @@ -722,7 +722,7 @@ function setDefaultRpcTarget (rpcList) { } function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) + log.debug(`background.setRpcTarget: ${newRpc}`) return (dispatch) => { background.setCustomRpc(newRpc, (err, result) => { if (err) { diff --git a/ui/app/app.js b/ui/app/app.js index 1a63002e1..4565bdd37 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -3,14 +3,12 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') // init const InitializeMenuScreen = require('./first-time/init-menu') const NewKeyChainScreen = require('./new-keychain') // unlock const UnlockScreen = require('./unlock') // accounts -const AccountsScreen = require('./accounts') const AccountDetailScreen = require('./account-detail') const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') @@ -24,15 +22,15 @@ const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') +const Dropdown = require('./components/dropdown').Dropdown +const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') const BuyView = require('./components/buy-button-subview') const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns module.exports = connect(mapStateToProps)(App) @@ -40,6 +38,13 @@ inherits(App, Component) function App () { Component.call(this) } function mapStateToProps (state) { + const { + identities, + accounts, + address, + } = state.metamask + const selected = address || Object.keys(accounts)[0] + return { // state from plugin isLoading: state.appState.isLoading, @@ -60,6 +65,10 @@ function mapStateToProps (state) { lastUnreadNotice: state.metamask.lastUnreadNotice, lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], + + // state needed to get account dropdown temporarily rendering from app bar + identities, + selected, } } @@ -69,16 +78,16 @@ App.prototype.render = function () { const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? `Connecting to ${this.getNetworkName()}` : null - log.debug('Main ui render function') return ( - h('.flex-column.flex-grow.full-height', { + h('.flex-column.full-height', { style: { // Windows was showing a vertical scroll bar: overflow: 'hidden', position: 'relative', + alignItems: 'center', }, }, [ @@ -93,20 +102,12 @@ App.prototype.render = function () { }), // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + h('.app-primary' + (transForward ? '.from-right' : '.from-left'), { style: { - height: '380px', - width: '360px', + width: '100%', }, }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), + this.renderPrimary(), ]), ]) ) @@ -123,14 +124,16 @@ App.prototype.renderAppBar = function () { return ( - h('div', [ + h('.full-width', { + height: '38px', + }, [ h('.app-header.flex-row.flex-space-between', { style: { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: props.isUnlocked ? 'white' : 'none', - height: '36px', + height: '38px', position: 'relative', zIndex: 12, }, @@ -178,32 +181,26 @@ App.prototype.renderAppBar = function () { }, }, [ - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), + props.isUnlocked && h(AccountDropdowns, { + style: {}, + enableAccountsSelector: true, + identities: this.props.identities, + selected: this.props.selected, + network: this.props.network, + }, []), // hamburger props.isUnlocked && h(SandwichExpando, { + className: 'sandwich-expando', width: 16, barHeight: 2, padding: 0, isOpen: state.isMainMenuOpen, color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + onClick: () => { + this.setState({ + isMainMenuOpen: !state.isMainMenuOpen, + }) }, }), ]), @@ -214,84 +211,141 @@ App.prototype.renderAppBar = function () { App.prototype.renderNetworkDropdown = function () { const props = this.props + const { provider: { type: providerType, rpcTarget: activeNetwork } } = props const rpcList = props.frequentRpcList const state = this.state || {} const isOpen = state.isNetworkMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { + useCssTransition: true, isOpen, onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) + const { classList } = event.target + const isNotToggleElement = [ + classList.contains('menu-icon'), + classList.contains('network-name'), + classList.contains('network-indicator'), + ].filter(bool => bool).length === 0 + // classes from three constituent nodes of the toggle element + + if (isNotToggleElement) { + this.setState({ isNetworkMenuOpen: false }) + } }, zIndex: 11, style: { position: 'absolute', - left: 0, + left: '2px', top: '36px', }, innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + padding: '2px 16px 2px 0px', }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), + }, [ + + h( + DropdownMenuItem, + { + key: 'main', + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('mainnet')), + style: { + fontSize: '18px', + }, + }, + [ + h('.menu-icon.diamond'), + 'Main Ethereum Network', + providerType === 'mainnet' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + key: 'ropsten', + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('ropsten')), + style: { + fontSize: '18px', + }, + }, + [ + h('.menu-icon.red-dot'), + 'Ropsten Test Network', + providerType === 'ropsten' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + key: 'kovan', + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('kovan')), + style: { + fontSize: '18px', + }, + }, + [ + h('.menu-icon.hollow-diamond'), + 'Kovan Test Network', + providerType === 'kovan' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + key: 'rinkeby', + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('rinkeby')), + style: { + fontSize: '18px', + }, + }, + [ + h('.menu-icon.golden-square'), + 'Rinkeby Test Network', + providerType === 'rinkeby' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + key: 'default', + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setDefaultRpcTarget()), + style: { + fontSize: '18px', + }, + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Localhost 8545', + activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, + ] + ), this.renderCustomOption(props.provider), this.renderCommonRpc(rpcList, props.provider), - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => this.props.dispatch(actions.showConfigPage()), + style: { + fontSize: '18px', + }, + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Custom RPC', + activeNetwork === 'custom' ? h('.check', '✓') : null, + ] + ), ]) } @@ -300,54 +354,42 @@ App.prototype.renderDropdown = function () { const state = this.state || {} const isOpen = state.isMainMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { + useCssTransition: true, isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) + const classList = event.target.classList + const parentClassList = event.target.parentElement.classList + + const isToggleElement = classList.contains('sandwich-expando') || + parentClassList.contains('sandwich-expando') + + if (isOpen && !isToggleElement) { + this.setState({ isMainMenuOpen: false }) + } }, style: { position: 'absolute', - right: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + right: '2px', + top: '38px', }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Import Account', + innerStyle: {}, + }, [ + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showConfigPage()) }, + }, 'Settings'), - h(DropMenuItem, { - label: 'Lock', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.lockMetamask()) }, + }, 'Lock'), - h(DropMenuItem, { - label: 'Info/Help', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showInfoPage()) }, + }, 'Info/Help'), ]) } @@ -433,10 +475,6 @@ App.prototype.renderPrimary = function () { // show current view switch (props.currentView.name) { - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - case 'accountDetail': log.debug('rendering account detail screen') return h(AccountDetailScreen, {key: 'account-detail'}) @@ -525,6 +563,8 @@ App.prototype.toggleMetamaskActive = function () { App.prototype.renderCustomOption = function (provider) { const { rpcTarget, type } = provider + const props = this.props + if (type !== 'rpc') return null // Concatenate long URLs @@ -539,13 +579,19 @@ App.prototype.renderCustomOption = function (provider) { return null default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) + return h( + DropdownMenuItem, + { + key: rpcTarget, + onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)), + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + label, + h('.check', '✓'), + ] + ) } } @@ -571,21 +617,26 @@ App.prototype.getNetworkName = function () { } App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider const props = this.props + const rpcTarget = provider.rpcTarget return rpcList.map((rpc) => { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) + return h( + DropdownMenuItem, + { + key: `common${rpc}`, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + onClick: () => props.dispatch(actions.setRpcTarget(rpc)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + rpc, + rpcTarget === rpc ? h('.check', '✓') : null, + ] + ) } }) } diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js new file mode 100644 index 000000000..b23600e9b --- /dev/null +++ b/ui/app/components/account-dropdowns.js @@ -0,0 +1,289 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const actions = require('../actions') +const genAccountLink = require('../../lib/account-link.js') +const connect = require('react-redux').connect +const Dropdown = require('./dropdown').Dropdown +const DropdownMenuItem = require('./dropdown').DropdownMenuItem +const Identicon = require('./identicon') +const ethUtil = require('ethereumjs-util') +const copyToClipboard = require('copy-to-clipboard') + +class AccountDropdowns extends Component { + constructor (props) { + super(props) + this.state = { + accountSelectorActive: false, + optionsMenuActive: false, + } + this.accountSelectorToggleClassName = 'accounts-selector' + this.optionsMenuToggleClassName = 'fa-ellipsis-h' + } + + renderAccounts () { + const { identities, selected } = this.props + + return Object.keys(identities).map((key, index) => { + const identity = identities[key] + const isSelected = identity.address === selected + + return h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + style: { + marginTop: index === 0 ? '5px' : '', + fontSize: '24px', + }, + }, + [ + h( + Identicon, + { + address: identity.address, + diameter: 32, + style: { + marginLeft: '10px', + }, + }, + ), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, identity.name || ''), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + renderAccountSelector () { + const { actions } = this.props + const { accountSelectorActive } = this.state + + return h( + Dropdown, + { + useCssTransition: true, // Hardcoded because account selector is temporarily in app-header + style: { + marginLeft: '-238px', + marginTop: '38px', + minWidth: '180px', + overflowY: 'auto', + maxHeight: '300px', + width: '300px', + }, + innerStyle: { + padding: '8px 25px', + }, + isOpen: accountSelectorActive, + onClickOutside: (event) => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName) + if (accountSelectorActive && isNotToggleElement) { + this.setState({ accountSelectorActive: false }) + } + }, + }, + [ + ...this.renderAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + style: { + marginLeft: '10px', + }, + diameter: 32, + }, + ), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + style: { + marginLeft: '10px', + }, + diameter: 32, + }, + ), + h('span', { + style: { + marginLeft: '20px', + fontSize: '24px', + marginBottom: '5px', + }, + }, 'Import Account'), + ] + ), + ] + ) + } + + renderAccountOptions () { + const { actions } = this.props + const { optionsMenuActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-215px', + minWidth: '180px', + }, + isOpen: optionsMenuActive, + onClickOutside: () => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName) + if (optionsMenuActive && isNotToggleElement) { + this.setState({ optionsMenuActive: false }) + } + }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, identities } = this.props + var identity = identities[selected] + actions.showQrView(selected, identity ? identity.name : '') + }, + }, + 'Show QR Code', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ) + } + + render () { + const { style, enableAccountsSelector, enableAccountOptions } = this.props + const { optionsMenuActive, accountSelectorActive } = this.state + + return h( + 'span', + { + style: style, + }, + [ + enableAccountsSelector && h( + // 'i.fa.fa-angle-down', + 'div.cursor-pointer.color-orange.accounts-selector', + { + style: { + // fontSize: '1.8em', + background: 'url(images/switch_acc.svg) white center center no-repeat', + height: '25px', + width: '25px', + transform: 'scale(0.75)', + marginRight: '3px', + }, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: !accountSelectorActive, + optionsMenuActive: false, + }) + }, + }, + this.renderAccountSelector(), + ), + enableAccountOptions && h( + 'i.fa.fa-ellipsis-h', + { + style: { + marginRight: '0.5em', + fontSize: '1.8em', + }, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: false, + optionsMenuActive: !optionsMenuActive, + }) + }, + }, + this.renderAccountOptions() + ), + ] + ) + } +} + +AccountDropdowns.defaultProps = { + enableAccountsSelector: false, + enableAccountOptions: false, +} + +AccountDropdowns.propTypes = { + identities: PropTypes.objectOf(PropTypes.object), + selected: PropTypes.string, +} + +const mapDispatchToProps = (dispatch) => { + return { + actions: { + showConfigPage: () => dispatch(actions.showConfigPage()), + requestAccountExport: () => dispatch(actions.requestExportAccount()), + showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + addNewAccount: () => dispatch(actions.addNewAccount()), + showImportPage: () => dispatch(actions.showImportPage()), + showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), + }, + } +} + +module.exports = { + AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +} diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js index 394d878f7..330f73805 100644 --- a/ui/app/components/account-export.js +++ b/ui/app/components/account-export.js @@ -100,7 +100,7 @@ ExportAccountView.prototype.render = function () { textOverflow: 'ellipsis', overflow: 'hidden', webkitUserSelect: 'text', - width: '100%', + maxWidth: '275px', }, onClick: function (event) { copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js new file mode 100644 index 000000000..34c7149ee --- /dev/null +++ b/ui/app/components/dropdown.js @@ -0,0 +1,94 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const MenuDroppo = require('./menu-droppo') +const extend = require('xtend') + +const noop = () => {} + +class Dropdown extends Component { + render () { + const { isOpen, onClickOutside, style, innerStyle, children, useCssTransition } = this.props + + const innerStyleDefaults = extend({ + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, innerStyle) + + return h( + MenuDroppo, + { + useCssTransition, + isOpen, + zIndex: 11, + onClickOutside, + style, + innerStyle: innerStyleDefaults, + }, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } + li.dropdown-menu-item { color: rgb(185, 185, 185); } + ` + ), + ...children, + ] + ) + } +} + +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, + useCssTransition: false, +} + +Dropdown.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, +} + +class DropdownMenuItem extends Component { + render () { + const { onClick, closeMenu, children, style } = this.props + + return h( + 'li.dropdown-menu-item', + { + onClick: () => { + onClick() + closeMenu() + }, + style: Object.assign({ + listStyle: 'none', + padding: '8px 0px 8px 0px', + fontSize: '18px', + fontStyle: 'normal', + fontFamily: 'Montserrat Regular', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, style), + }, + children + ) + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, +} + +module.exports = { + Dropdown, + DropdownMenuItem, +} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js index 41936f5e0..167be7eaf 100644 --- a/ui/app/components/editable-label.js +++ b/ui/app/components/editable-label.js @@ -30,7 +30,12 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { - this.setState({ isEditingLabel: true }) + const nameAttribute = event.target.getAttribute('name') + // checks for class to handle smaller CTA above the account name + const classAttribute = event.target.getAttribute('class') + if (nameAttribute === 'edit' || classAttribute === 'edit-text') { + this.setState({ isEditingLabel: true }) + } }, }, this.props.children) } diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 87d6f5d20..163792584 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -1,7 +1,6 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') inherits(LoadingIndicator, Component) @@ -15,35 +14,28 @@ LoadingIndicator.prototype.render = function () { const { isLoading, loadingMessage } = this.props return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, + isLoading ? h('.full-flex-height', { + style: { + left: '0px', + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, }, [ + h('img', { + src: 'images/loading.svg', + }), - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null ) } diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/menu-droppo.js new file mode 100644 index 000000000..66ab18954 --- /dev/null +++ b/ui/app/components/menu-droppo.js @@ -0,0 +1,130 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + +module.exports = MenuDroppoComponent + + +inherits(MenuDroppoComponent, Component) +function MenuDroppoComponent () { + Component.call(this) +} + +MenuDroppoComponent.prototype.render = function () { + const speed = this.props.speed || '300ms' + const useCssTransition = this.props.useCssTransition + const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0 + + this.manageListeners() + + let style = this.props.style || {} + if (!('position' in style)) { + style.position = 'fixed' + } + style.zIndex = zIndex + + return ( + h('.menu-droppo-container', { + style, + }, [ + h('style', ` + .menu-droppo-enter { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + + .menu-droppo-enter.menu-droppo-enter-active { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave.menu-droppo-leave-active { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + `), + + useCssTransition + ? h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'menu-droppo', + transitionEnterTimeout: parseInt(speed), + transitionLeaveTimeout: parseInt(speed), + }, this.renderPrimary()) + : this.renderPrimary(), + ]) + ) +} + +MenuDroppoComponent.prototype.renderPrimary = function () { + const isOpen = this.props.isOpen + if (!isOpen) { + return null + } + + const innerStyle = this.props.innerStyle || {} + + return ( + h('.menu-droppo', { + key: 'menu-droppo-drawer', + style: innerStyle, + }, + [ this.props.children ]) + ) +} + +MenuDroppoComponent.prototype.manageListeners = function () { + const isOpen = this.props.isOpen + const onClickOutside = this.props.onClickOutside + + if (isOpen) { + this.outsideClickHandler = onClickOutside + } else if (isOpen) { + this.outsideClickHandler = null + } +} + +MenuDroppoComponent.prototype.componentDidMount = function () { + if (this && document.body) { + this.globalClickHandler = this.globalClickOccurred.bind(this) + document.body.addEventListener('click', this.globalClickHandler) + var container = findDOMNode(this) + this.container = container + } +} + +MenuDroppoComponent.prototype.componentWillUnmount = function () { + if (this && document.body) { + document.body.removeEventListener('click', this.globalClickHandler) + } +} + +MenuDroppoComponent.prototype.globalClickOccurred = function (event) { + const target = event.target + const container = findDOMNode(this) + + if (target !== container && + !isDescendant(this.container, event.target) && + this.outsideClickHandler) { + this.outsideClickHandler(event) + } +} + +function isDescendant (parent, child) { + var node = child.parentNode + while (node !== null) { + if (node === parent) { + return true + } + node = node.parentNode + } + + return false +} diff --git a/ui/app/components/network.js b/ui/app/components/network.js index d5d3e18cd..698a0bbb9 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -39,7 +39,6 @@ Network.prototype.render = function () { }), h('i.fa.fa-sort-desc'), ]) - } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index d9f0067cd..c26505193 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -19,7 +19,11 @@ Notice.prototype.render = function () { const disabled = state.disclaimerDisabled return ( - h('.flex-column.flex-center.flex-grow', [ + h('.flex-column.flex-center.flex-grow', { + style: { + width: '100%', + }, + }, [ h('h3.flex-center.text-transform-uppercase.terms-header', { style: { background: '#EBEBEB', diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index e0a720426..76a265d63 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -2,7 +2,6 @@ const PersistentForm = require('../../lib/persistent-form') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const actions = require('../actions') const Qr = require('./qr-code') const isValidAddress = require('../util').isValidAddress @@ -24,14 +23,7 @@ function ShapeshiftForm () { } ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) + return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain() } ShapeshiftForm.prototype.renderMain = function () { diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index 6295e7dd9..bef444a48 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -21,6 +21,7 @@ TabBar.prototype.render = function () { background: '#EBEBEB', color: '#AEAEAE', paddingTop: '4px', + minHeight: '30px', }, }, tabs.map((tab) => { const { key, content } = tab diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 20cfa897e..5ea31ae8d 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -47,10 +47,11 @@ TokenList.prototype.render = function () { return h(TokenCell, tokenData) }) - return h('div', [ - h('ol', { + return h('.full-flex-height', [ + this.renderTokenStatusBar(), + + h('ol.full-flex-height.flex-column', { style: { - height: '260px', overflowY: 'auto', display: 'flex', flexDirection: 'column', @@ -63,6 +64,7 @@ TokenList.prototype.render = function () { flex-direction: row; align-items: center; padding: 10px; + min-height: 50px; } li.token-cell > h3 { @@ -76,17 +78,37 @@ TokenList.prototype.render = function () { `), ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), + h('.flex-grow'), ]), - this.addTokenButtonElement(), ]) } -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { +TokenList.prototype.renderTokenStatusBar = function () { + const { tokens } = this.state + + let msg + if (tokens.length === 1) { + msg = `You own 1 token` + } else if (tokens.length === 1) { + msg = `You own ${tokens.length} tokens` + } else { + msg = `No tokens found` + } + + return h('div', { + style: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + minHeight: '70px', + padding: '10px', + }, + }, [ + h('span', msg), + h('button', { key: 'reveal-account-bar', - onClick: () => { + onClick: (event) => { + event.preventDefault() this.props.addToken() }, style: { @@ -97,7 +119,7 @@ TokenList.prototype.addTokenButtonElement = function () { alignItems: 'center', }, }, [ - h('i.fa.fa-plus.fa-lg'), + 'ADD TOKEN', ]), ]) } diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 3b4ba741e..69b72614c 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -24,7 +24,11 @@ TransactionList.prototype.render = function () { return ( - h('section.transaction-list', [ + h('section.transaction-list.full-flex-height', { + style: { + justifyContent: 'center', + }, + }, [ h('style', ` .transaction-list .transaction-list-item:not(:last-of-type) { @@ -39,7 +43,7 @@ TransactionList.prototype.render = function () { h('.tx-list', { style: { overflowY: 'auto', - height: '300px', + height: '100%', padding: '0 20px', textAlign: 'center', }, @@ -64,13 +68,17 @@ TransactionList.prototype.render = function () { }, }) }) - : h('.flex-center', { + : h('.flex-center.full-flex-height', { style: { flexDirection: 'column', - height: '100%', + justifyContent: 'center', }, }, [ - 'No transaction history.', + h('p', { + style: { + marginTop: '50px', + }, + }, 'No transaction history.'), ]), ]), ]) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 747d3ce2b..34727ff78 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -1,6 +1,5 @@ const inherits = require('util').inherits const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') @@ -92,34 +91,25 @@ ConfirmTxScreen.prototype.render = function () { warningIfExists(props.warning), - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), ]) ) } diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 808aafb4c..49b432a1f 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -19,17 +19,52 @@ html, body { font-weight: 300; line-height: 1.4em; background: #F7F7F7; + margin: 0; + padding: 0; +} + +html { + min-height: 500px; +} + +.app-root { + overflow: hidden; + position: relative +} + +.app-primary { + display: flex; } input:focus, textarea:focus { outline: none; } +.full-size { + height: 100%; + width: 100%; +} + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.full-flex-height { + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + #app-content { overflow-x: hidden; min-width: 357px; - width: 360px; - height: 500px; + height: 100%; + display: flex; + flex-direction: column; } button, input[type="submit"] { @@ -130,10 +165,6 @@ h2.page-subtitle { margin: 12px; } -.app-primary { - -} - .app-footer { padding-bottom: 10px; align-items: center; @@ -170,7 +201,7 @@ textarea.twelve-word-phrase { } .check { - margin-left: 7px; + margin-left: 12px; color: #F7861C; flex: 1 0 auto; display: flex; @@ -403,8 +434,16 @@ input.large-input { /* account detail screen */ .account-detail-section { + display: flex; + flex-wrap: wrap; + overflow-y: auto; + flex-direction: inherit; +} +.grow-tenx { + flex-grow: 10; } + .name-label{ } diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 910a24ee2..81647d1c1 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -232,12 +232,21 @@ hr.horizontal-line { align-items: center; } +.tabSection { + min-width: 350px; +} + .menu-icon { display: inline-block; - height: 9px; - min-width: 9px; + height: 12px; + min-width: 12px; margin: 13px; } + +i.fa.fa-question-circle.fa-lg.menu-icon { + font-size: 18px; +} + .ether-icon { background: rgb(0, 163, 68); border-radius: 20px; @@ -266,3 +275,31 @@ hr.horizontal-line { margin-top: 20px; color: red; } + +/* + Hacky breakpoint fix for account + tab sections + Resolves issue from @frankiebee in + https://github.com/MetaMask/metamask-extension/pull/1835 + Please remove this when integrating new designs + */ + +@media screen and (min-width: 575px) and (max-width: 800px) { + .account-data-subsection { + flex: 0 0 auto !important; // reset flex + margin-left: 10px !important; // create additional horizontal space + margin-right: 10px !important; + width: 40%; + } + + .tabSection { + flex: 0 0 auto !important; + margin-left: 10px !important; + margin-right: 10px !important; + min-width: 285px; + width: 49%; + } + + .name-label { + width: 80%; + } +} diff --git a/ui/app/info.js b/ui/app/info.js index cb2e41f5b..899841c83 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -20,7 +20,11 @@ InfoScreen.prototype.render = function () { const version = global.platform.getVersion() return ( - h('.flex-column.flex-grow', [ + h('.flex-column.flex-grow', { + style: { + maxWidth: '400px', + }, + }, [ // subtitle and nav h('.section-title.flex-row.flex-center', [ @@ -103,12 +107,7 @@ InfoScreen.prototype.render = function () { target: '_blank', }, 'Visit our Support Center'), ]), - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/metamask-extension/issues/new', - target: '_blank', - }, 'Found a bug? Report it!'), - ]), + h('div', [ h('a', { href: 'https://metamask.io/', @@ -126,6 +125,7 @@ InfoScreen.prototype.render = function () { h('div.info', 'Visit our web site'), ]), ]), + h('div.fa.fa-slack', [ h('a.info', { href: 'http://slack.metamask.io', @@ -133,11 +133,13 @@ InfoScreen.prototype.render = function () { }, 'Join the conversation on Slack'), ]), - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), + h('div', [ + h('.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), ]), h('div.fa.fa-envelope', [ diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index a318a9b50..c32751fff 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -47,8 +47,6 @@ CreateVaultCompleteScreen.prototype.render = function () { h('div', { style: { - width: '360px', - height: '78px', fontSize: '1em', marginTop: '10px', textAlign: 'center', diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js index 4ccbec9fc..4335186a5 100644 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ b/ui/app/keychains/hd/recover-seed/confirmation.js @@ -23,7 +23,9 @@ RevealSeedConfirmation.prototype.render = function () { return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', [ + h('.initialize-screen.flex-column.flex-center.flex-grow', { + style: { maxWidth: '420px' }, + }, [ h('h3.flex-center.text-transform-uppercase', { style: { @@ -61,7 +63,7 @@ RevealSeedConfirmation.prototype.render = function () { }, }), - h('.flex-row.flex-space-between', { + h('.flex-row.flex-start', { style: { marginTop: 30, width: '50%', @@ -74,6 +76,7 @@ RevealSeedConfirmation.prototype.render = function () { // submit h('button.primary', { + style: { marginLeft: '10px' }, onClick: this.revealSeedWords.bind(this), }, 'OK'), diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 1aee3c5d0..9bacd5124 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -26,7 +26,11 @@ UnlockScreen.prototype.render = function () { const state = this.props const warning = state.warning return ( - h('.flex-column', [ + h('.flex-column', { + style: { + width: 'inherit', + }, + }, [ h('.unlock-screen.flex-column.flex-center.flex-grow', [ h(Mascot, { diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index afc62e7b6..5def23e51 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -18,4 +18,3 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) return allValues } - |