diff options
author | Chi Kei Chan <chikeichan@gmail.com> | 2017-10-19 14:04:54 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-19 14:04:54 +0800 |
commit | 776e8241f0464939cc3049f7155c150e3375c623 (patch) | |
tree | 017600778515e74eaf15bf4f889e5b26638c56d7 /ui | |
parent | de1da7d1b215ade650fc644adf63384a401dc240 (diff) | |
parent | a8bba2f4b7ca714d46b2e1ca405aec3135aa23b2 (diff) | |
download | tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.tar tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.tar.gz tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.tar.bz2 tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.tar.lz tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.tar.xz tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.tar.zst tangerine-wallet-browser-776e8241f0464939cc3049f7155c150e3375c623.zip |
Merge pull request #2402 from chikeichan/merge
[NewUI] Merge master into NewUI-flat
Diffstat (limited to 'ui')
-rw-r--r-- | ui/app/actions.js | 27 | ||||
-rw-r--r-- | ui/app/components/account-dropdowns.js | 317 | ||||
-rw-r--r-- | ui/app/components/bn-as-decimal-input.js | 11 | ||||
-rw-r--r-- | ui/app/components/pending-typed-msg-details.js | 59 | ||||
-rw-r--r-- | ui/app/components/pending-typed-msg.js | 46 | ||||
-rw-r--r-- | ui/app/components/shift-list-item.js | 2 | ||||
-rw-r--r-- | ui/app/components/transaction-list-item.js | 2 | ||||
-rw-r--r-- | ui/app/components/typed-message-renderer.js | 42 | ||||
-rw-r--r-- | ui/app/conf-tx.js | 22 | ||||
-rw-r--r-- | ui/app/reducers/app.js | 4 | ||||
-rw-r--r-- | ui/index.js | 2 | ||||
-rw-r--r-- | ui/lib/explorer-link.js | 6 | ||||
-rw-r--r-- | ui/lib/tx-helper.js | 13 |
13 files changed, 534 insertions, 19 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 25a4abda6..4c83f95b4 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -118,6 +118,8 @@ var actions = { cancelMsg: cancelMsg, signPersonalMsg, cancelPersonalMsg, + signTypedMsg, + cancelTypedMsg, sendTx: sendTx, signTx: signTx, signTokenTx: signTokenTx, @@ -462,6 +464,25 @@ function signPersonalMsg (msgData) { } } +function signTypedMsg (msgData) { + log.debug('action - signTypedMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signTypedMessage`) + background.signTypedMessage(msgData, (err, newState) => { + log.debug('signTypedMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + function signTx (txData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -633,6 +654,12 @@ function cancelPersonalMsg (msgData) { return actions.completedTx(id) } +function cancelTypedMsg (msgData) { + const id = msgData.id + background.cancelTypedMessage(id) + return actions.completedTx(id) +} + function cancelTx (txData) { return (dispatch) => { log.debug(`background.cancelTransaction`) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js new file mode 100644 index 000000000..1b46e532a --- /dev/null +++ b/ui/app/components/account-dropdowns.js @@ -0,0 +1,317 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const actions = require('../actions') +const genAccountLink = require('etherscan-link').createAccountLink +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, keyrings } = this.props + + return Object.keys(identities).map((key, index) => { + const identity = identities[key] + const isSelected = identity.address === selected + + const simpleAddress = identity.address.substring(2).toLowerCase() + + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + 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', + }, + }, + ), + this.indicateIfLoose(keyring), + h('span', { + style: { + marginLeft: '20px', + fontSize: '24px', + maxWidth: '145px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, identity.name || ''), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + indicateIfLoose (keyring) { + try { // Sometimes keyrings aren't loaded yet: + const type = keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } + } + + 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, + keyrings: PropTypes.array, +} + +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/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index f3ace4720..d84834d06 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -31,7 +31,7 @@ BnAsDecimalInput.prototype.render = function () { const suffix = props.suffix const style = props.style const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) + const newValue = this.downsize(valueString, scale) return ( h('.flex-column', [ @@ -145,14 +145,17 @@ BnAsDecimalInput.prototype.constructWarning = function () { } -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { +BnAsDecimalInput.prototype.downsize = function (number, scale) { // if there is no scaling, simply return the number if (scale === 0) { return Number(number) } else { // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + var adjustedNumber = number + while (adjustedNumber.length < scale) { + adjustedNumber = '0' + adjustedNumber + } + return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale)) } } diff --git a/ui/app/components/pending-typed-msg-details.js b/ui/app/components/pending-typed-msg-details.js new file mode 100644 index 000000000..b5fd29f71 --- /dev/null +++ b/ui/app/components/pending-typed-msg-details.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const TypedMessageRenderer = require('./typed-message-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'), + h(TypedMessageRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} diff --git a/ui/app/components/pending-typed-msg.js b/ui/app/components/pending-typed-msg.js new file mode 100644 index 000000000..f8926d0a3 --- /dev/null +++ b/ui/app/components/pending-typed-msg.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-typed-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelTypedMessage, + }, 'Cancel'), + h('button', { + onClick: state.signTypedMessage, + }, 'Sign'), + ]), + ]) + + ) +} diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 392b67630..43973de63 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -3,7 +3,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const vreme = new (require('vreme'))() -const explorerLink = require('../../lib/explorer-link') +const explorerLink = require('etherscan-link').createExplorerLink const actions = require('../actions') const addressSummary = require('../util').addressSummary diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 3ea466d24..21f2b8236 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -4,7 +4,7 @@ const inherits = require('util').inherits const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') +const explorerLink = require('etherscan-link').createExplorerLink const CopyButton = require('./copyButton') const vreme = new (require('vreme'))() const Tooltip = require('./tooltip') diff --git a/ui/app/components/typed-message-renderer.js b/ui/app/components/typed-message-renderer.js new file mode 100644 index 000000000..a042b57be --- /dev/null +++ b/ui/app/components/typed-message-renderer.js @@ -0,0 +1,42 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') + +module.exports = TypedMessageRenderer + +inherits(TypedMessageRenderer, Component) +function TypedMessageRenderer () { + Component.call(this) +} + +TypedMessageRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = renderTypedData(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + overflow: 'scroll', + }, style) + + return ( + h('div.font-small', { + style: defaultStyle, + }, text) + ) +} + +function renderTypedData(values) { + return values.map(function (value) { + return h('div', {}, [ + h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'), + h('div', {}, value.value), + ]) + }) +}
\ No newline at end of file diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 18645dc69..f201010fc 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -8,6 +8,7 @@ const txHelper = require('../lib/tx-helper') const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') const PendingPersonalMsg = require('./components/pending-personal-msg') +const PendingTypedMsg = require('./components/pending-typed-msg') const Loading = require('./components/loading') // const contentDivider = h('div', { @@ -29,6 +30,7 @@ function mapStateToProps (state) { unapprovedTxs: state.metamask.unapprovedTxs, unapprovedMsgs: state.metamask.unapprovedMsgs, unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + unapprovedTypedMessages: state.metamask.unapprovedTypedMessages, index: state.appState.currentView.context, warning: state.appState.warning, network: state.metamask.network, @@ -53,13 +55,14 @@ ConfirmTxScreen.prototype.render = function () { currentCurrency, unapprovedMsgs, unapprovedPersonalMsgs, + unapprovedTypedMessages, conversionRate, blockGasLimit, // provider, // computedBalances, } = props - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) var txData = unconfTxList[props.index] || {} var txParams = txData.params || {} @@ -122,6 +125,9 @@ function currentTxView (opts) { } else if (type === 'personal_sign') { log.debug('rendering personal_sign message') return h(PendingPersonalMsg, opts) + } else if (type === 'eth_signTypedData') { + log.debug('rendering eth_signTypedData message') + return h(PendingTypedMsg, opts) } } return h(Loading, { isLoading: true }) @@ -171,6 +177,14 @@ ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { this.props.dispatch(actions.signPersonalMsg(params)) } +ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) { + log.info('conf-tx.js: signing typed message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signTypedMsg(params)) +} + ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { log.info('canceling message') this.stopPropagation(event) @@ -183,6 +197,12 @@ ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { this.props.dispatch(actions.cancelPersonalMsg(msgData)) } +ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) { + log.info('canceling typed message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelTypedMsg(msgData)) +} + ConfirmTxScreen.prototype.goHome = function (event) { this.stopPropagation(event) this.props.dispatch(actions.goHome()) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 4f10d9857..f10bf9fb7 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -637,9 +637,9 @@ function checkUnconfActions (state) { function getUnconfActionList (state) { const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask + unapprovedPersonalMsgs, unapprovedTypedMessages, network } = state.metamask - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) return unconfActionList } diff --git a/ui/index.js b/ui/index.js index 1afc08683..fff677471 100644 --- a/ui/index.js +++ b/ui/index.js @@ -37,7 +37,7 @@ function startApp (metamaskState, accountManager, opts) { }) // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const numberOfUnapprivedTx = unapprovedTxsAll.length if (numberOfUnapprivedTx > 0) { store.dispatch(actions.showConfTxPage({ diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js deleted file mode 100644 index 3b82ecd5f..000000000 --- a/ui/lib/explorer-link.js +++ /dev/null @@ -1,6 +0,0 @@ -const prefixForNetwork = require('./etherscan-prefix-for-network') - -module.exports = function (hash, network) { - const prefix = prefixForNetwork(network) - return `http://${prefix}etherscan.io/tx/${hash}` -} diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index 5def23e51..341567e2f 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -1,20 +1,27 @@ const valuesFor = require('../app/util').valuesFor -module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network) { log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network }) const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) log.debug(`tx helper found ${msgValues.length} unsigned messages`) let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) allValues = allValues.concat(personalValues) + + const typedValues = valuesFor(typedMessages) + log.debug(`tx helper found ${typedValues.length} unsigned typed messages`) + allValues = allValues.concat(typedValues) + allValues = allValues.sort((a, b) => { return a.time > b.time }) return allValues -} +}
\ No newline at end of file |