1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Merge branch 'master' of github.com:MetaMask/metamask-extension into greenkeeper/initial

This commit is contained in:
kumavis 2017-08-07 10:52:08 -07:00
commit 78aa957e5a
39 changed files with 1173 additions and 739 deletions

View File

@ -2,6 +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 ujo token
- Continuously update blacklist for known phishing sites in background.
- Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them.
@ -75,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.
@ -231,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
@ -254,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.
@ -310,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
@ -386,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
@ -418,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

12
app/home.html Normal file
View File

@ -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>

View File

@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "3.9.2",
"version": "3.9.5",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",

View File

@ -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>
</html>

View File

@ -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)

View File

@ -1,9 +1,10 @@
const promiseToCallback = require('promise-to-callback')
module.exports = function(fn, context) {
module.exports = function nodeify (fn, context) {
return function(){
const args = [].slice.call(arguments)
const callback = args.pop()
if (typeof callback !== 'function') throw new Error('callback is not a function')
promiseToCallback(fn.apply(context, args))(callback)
}
}

View File

@ -68,7 +68,7 @@
"eth-bin-to-ops": "^1.0.1",
"eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.1.1",
"eth-phishing-detect": "^1.0.2",
"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": "^3.0.0",
"react-markdown": "^2.3.0",

View File

@ -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
})

View File

@ -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)

View File

@ -45,13 +45,15 @@ describe('tx confirmation screen', function () {
before(function (done) {
actions._setBackgroundConnection({
approveTransaction (txId, cb) { cb('An error!') },
cancelTransaction (txId) { /* noop */ },
cancelTransaction (txId, cb) { cb() },
clearSeedWordCache (cb) { cb() },
})
const action = actions.cancelTx({value: firstTxId})
result = reducers(initialState, action)
done()
actions.cancelTx({value: firstTxId})((action) => {
result = reducers(initialState, action)
done()
})
})
it('should transition to the account detail view', function () {

View File

@ -17,4 +17,15 @@ describe('nodeify', function () {
done()
})
})
it('should throw if the last argument is not a function', function (done) {
const nodified = nodeify(obj.promiseFunc, obj)
try {
nodified('baz')
done(new Error('should have thrown if the last argument is not a function'))
} catch (err) {
assert.equal(err.message, 'callback is not a function')
done()
}
})
})

View File

@ -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);
});
});

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -462,9 +462,12 @@ function cancelPersonalMsg (msgData) {
}
function cancelTx (txData) {
log.debug(`background.cancelTransaction`)
background.cancelTransaction(txData.id)
return actions.completedTx(txData.id)
return (dispatch) => {
log.debug(`background.cancelTransaction`)
background.cancelTransaction(txData.id, () => {
dispatch(actions.completedTx(txData.id))
})
}
}
//
@ -706,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) => {
@ -719,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) {

View File

@ -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(
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(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(
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(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(
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(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(
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(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: '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',
right: '2px',
top: '38px',
},
innerStyle: {
background: 'white',
boxShadow: '1px 1px 2px rgba(0,0,0,0.1)',
},
}, [ // DROP MENU ITEMS
h('style', `
.drop-menu-item:hover { background:rgb(235, 235, 235); }
.drop-menu-item i { margin: 11px; }
`),
h(DropMenuItem, {
label: 'Settings',
innerStyle: {},
}, [
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-gear.fa-lg'),
}),
onClick: () => { this.props.dispatch(actions.showConfigPage()) },
}, 'Settings'),
h(DropMenuItem, {
label: 'Import Account',
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.lockMetamask()) },
}, 'Lock'),
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'),
}),
h(DropMenuItem, {
label: 'Info/Help',
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,
]
)
}
})
}

View File

@ -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),
}

View File

@ -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))

View File

@ -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 }) },
}),
]),
])
}

View File

@ -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', '✓')
}
}

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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'),
h('br'),
showMessageIfAny(loadingMessage),
]) : null,
])
showMessageIfAny(loadingMessage),
]) : null
)
}

View File

@ -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
}

View File

@ -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'

View File

@ -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',

View File

@ -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 () {

View File

@ -21,6 +21,7 @@ TabBar.prototype.render = function () {
background: '#EBEBEB',
color: '#AEAEAE',
paddingTop: '4px',
minHeight: '30px',
},
}, tabs.map((tab) => {
const { key, content } = tab

View File

@ -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',
]),
])
}

View File

@ -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.'),
]),
]),
])

View File

@ -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),
}),
])
)
}

View File

@ -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{
}

View File

@ -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%;
}
}

View File

@ -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', [

View File

@ -47,8 +47,6 @@ CreateVaultCompleteScreen.prototype.render = function () {
h('div', {
style: {
width: '360px',
height: '78px',
fontSize: '1em',
marginTop: '10px',
textAlign: 'center',

View File

@ -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'),

View File

@ -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, {

View File

@ -18,4 +18,3 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network)
return allValues
}