Merge pull request #406 from MetaMask/ConfirmationStyle

Update transaction confirmation style
This commit is contained in:
Dan Finlay 2016-07-07 15:11:21 -07:00 committed by GitHub
commit 2bd31c3e50
21 changed files with 607 additions and 77 deletions

View File

@ -6,6 +6,7 @@
- Fix formatting of account details.
- Use web3 minified dist for faster inject times
- Fix issue where dropdowns were not in front of icons.
- Update transaction approval styles.
- Align failed and successful transaction history text.
## 2.5.0 2016-06-29

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="4.2333331mm"
height="12.800793mm"
viewBox="0 0 14.999999 45.357139"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="forward-carrat.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="17.87049"
inkscape:cy="17.678567"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
showguides="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1276"
inkscape:window-height="755"
inkscape:window-x="4"
inkscape:window-y="1"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4136"
originx="-180"
originy="-602.14286" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-180,-404.8622)">
<path
style="fill:#f7861c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 180,404.8622 0,7.5 10,15 -10,15 0,7.85714 15,-22.85714 z"
id="path4138"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -18,7 +18,7 @@ html, body, #app-content, .super-dev-container {
height: 100%;
width: 100%;
position: relative;
background: #cccccc;
background: white;
}
.mock-app-root {
background: #F7F7F7;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,84 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"name": "Wallet 1",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"mayBeFauceting": false
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"name": "Wallet 2",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b",
"mayBeFauceting": false
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"name": "Wallet 3",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823",
"mayBeFauceting": false
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"name": "Wallet 4",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"accounts": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"code": "0x",
"balance": "0x01",
"nonce": "0x0",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"code": "0x",
"nonce": "0x0",
"balance": "0x01",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"code": "0x",
"nonce": "0x0",
"balance": "0x01",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823"
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69"
}
},
"transactions": [],
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"detailView": null,
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
var assert = require('assert')
var sinon = require('sinon')
var path = require('path')
var contractNamer = require(path.join(__dirname, '..', '..', 'ui', 'lib', 'contract-namer.js'))
describe('contractNamer', function() {
beforeEach(function() {
this.sinon = sinon.sandbox.create()
})
afterEach(function() {
this.sinon.restore()
})
describe('naming a contract', function() {
it('should return nothing for an unknown random account', function() {
const input = '0x2386F26FC10000'
const output = contractNamer(input)
assert.deepEqual(output, null)
})
it('should accept identities as an optional second parameter', function() {
const input = '0x2386F26FC10000'.toLowerCase()
const expected = 'bar'
const identities = {}
identities[input] = { name: expected }
const output = contractNamer(input, identities)
assert.deepEqual(output, expected)
})
it('should check for identities case insensitively', function() {
const input = '0x2386F26FC10000'.toLowerCase()
const expected = 'bar'
const identities = {}
identities[input] = { name: expected }
const output = contractNamer(input.toUpperCase(), identities)
assert.deepEqual(output, expected)
})
})
})

View File

@ -52,6 +52,12 @@ describe('util', function() {
var result = util.addressSummary(address)
assert.equal(result, '0xFDEa65C8...b825')
})
it('should accept arguments for firstseg, lastseg, and keepPrefix', function() {
var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'
var result = util.addressSummary(address, 4, 4, false)
assert.equal(result, 'FDEa...b825')
})
})
describe('isValidAddress', function() {

View File

@ -1,3 +1,20 @@
/* UI DEV
*
* This is a utility module.
* It initializes a minimalist browserifiable project
* that contains the Metamask UI, with a mocked state.
*
* Includes a state menu for switching between different
* mocked states, along with query param support,
* so those states are preserved when live-reloading.
*
* This is a convenient way to develop on the UI
* without having to re-enter your password
* every time the plugin rebuilds.
*
* To use, run `npm run ui`.
*/
const render = require('react-dom').render
const h = require('react-hyperscript')
const Root = require('./ui/app/root')
@ -54,7 +71,7 @@ render(
style: {
height: '500px',
width: '360px',
boxShadow: '2px 2px 5px grey',
boxShadow: 'grey 0px 2px 9px',
margin: '20px',
},
}, [

View File

@ -22,7 +22,7 @@ AccountPanel.prototype.render = function () {
var panelState = {
key: `accountPanel${identity.address}`,
identiconKey: identity.address,
identiconLabel: identity.name,
identiconLabel: identity.name || '',
attributes: [
{
key: 'ADDRESS',

View File

@ -12,9 +12,11 @@ function EthBalanceComponent () {
}
EthBalanceComponent.prototype.render = function () {
var state = this.props
var style = state.style
var value = formatBalance(state.value)
var props = this.props
var style = props.style
const value = formatBalance(props.value)
return (
h('.ether-balance', {
@ -30,33 +32,49 @@ EthBalanceComponent.prototype.render = function () {
)
}
EthBalanceComponent.prototype.renderBalance = function (value) {
const props = this.props
if (value === 'None') return value
var balanceObj = generateBalanceObject(value)
var balance = balanceObj.balance
var label = balanceObj.label
var tagName = props.inline ? 'span' : 'div'
var topTag = props.inline ? 'div' : '.flex-column'
return (
h(Tooltip, {
position: 'bottom',
title: value.split(' ')[0],
}, [
h('.flex-column', {
h(topTag, {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
lineHeight: props.fontSize || '13px',
fontFamily: 'Montserrat Regular',
textRendering: 'geometricPrecision',
},
}, [
h('div', balance),
h('div', {
h(tagName, {
style: {
color: ' #AEAEAE',
fontSize: '12px',
fontSize: props.fontSize || '12px',
},
}, label),
}, balance + ' '),
h(tagName, {
style: {
color: props.labelColor || '#AEAEAE',
fontSize: props.fontSize || '12px',
},
}, [
h('div', balance),
h('div', {
style: {
color: '#AEAEAE',
fontSize: '12px',
},
}, label),
]),
]),
])
)
}

View File

@ -0,0 +1,74 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const Identicon = require('./identicon')
module.exports = AccountPanel
inherits(AccountPanel, Component)
function AccountPanel () {
Component.call(this)
}
AccountPanel.prototype.render = function () {
var props = this.props
var picOrder = props.picOrder || 'left'
const { imageSeed } = props
return (
h('.identity-panel.flex-row.flex-left', {
style: {
cursor: props.onClick ? 'pointer' : undefined,
},
onClick: props.onClick,
}, [
this.genIcon(imageSeed, picOrder),
h('div.flex-column.flex-justify-center', {
style: {
lineHeight: '15px',
order: 2,
display: 'flex',
alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end',
},
}, this.props.children),
])
)
}
AccountPanel.prototype.genIcon = function (seed, picOrder) {
const props = this.props
// When there is no seed value, this is a contract creation.
// We then show the contract icon.
if (!seed) {
return h('.identicon-wrapper.flex-column.select-none', {
style: {
order: picOrder === 'left' ? 1 : 3,
},
}, [
h('i.fa.fa-file-text-o.fa-lg', {
style: {
fontSize: '42px',
transform: 'translate(0px, -16px)',
},
}),
])
}
// If there was a seed, we return an identicon for that address.
return h('.identicon-wrapper.flex-column.select-none', {
style: {
order: picOrder === 'left' ? 1 : 3,
},
}, [
h(Identicon, {
address: seed,
imageify: props.imageifyIdenticons,
}),
])
}

View File

@ -2,10 +2,17 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
const MiniAccountPanel = require('./mini-account-panel')
const EtherBalance = require('./eth-balance')
const addressSummary = require('../util').addressSummary
const readableDate = require('../util').readableDate
const formatBalance = require('../util').formatBalance
const nameForAddress = require('../../lib/contract-namer')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const baseGasFee = new BN('21000', 10)
const gasCost = new BN('4a817c800', 16)
const baseFeeHex = baseGasFee.mul(gasCost).toString(16)
module.exports = PendingTxDetails
@ -14,52 +21,204 @@ function PendingTxDetails () {
Component.call(this)
}
PendingTxDetails.prototype.render = function () {
var state = this.props
return this.renderGeneric(h, state)
}
const PTXP = PendingTxDetails.prototype
PendingTxDetails.prototype.renderGeneric = function (h, state) {
var txData = state.txData
PTXP.render = function () {
var props = this.props
var txData = props.txData
var txParams = txData.txParams || {}
var address = txParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
var address = txParams.from || props.selectedAddress
var identity = props.identities[address] || { address: address }
var balance = props.accounts[address].balance
var gasCost = ethUtil.stripHexPrefix(txParams.gas || baseFeeHex)
var txValue = ethUtil.stripHexPrefix(txParams.value || '0x0')
var maxCost = ((new BN(txValue, 16)).add(new BN(gasCost, 16))).toString(16)
var dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0
return (
h('div', [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
imageifyIdenticons: state.imageifyIdenticons,
}),
h('.flex-row.flex-center', {
style: {
maxWidth: '100%',
},
}, [
// tx data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h(MiniAccountPanel, {
imageSeed: address,
imageifyIdenticons: props.imageifyIdenticons,
picOrder: 'right',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, identity.name),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, addressSummary(address, 6, 4, false)),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, h(EtherBalance, {
value: balance,
inline: true,
})),
h('.flex-row.flex-space-between', [
h('label.font-small', 'TO ADDRESS'),
h('span.font-small', addressSummary(txParams.to)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'DATE'),
h('span.font-small', readableDate(txData.time)),
]),
h('img', {
src: 'images/forward-carrat.svg',
style: {
padding: '5px 6px 0px 10px',
height: '37px',
},
}),
h('.flex-row.flex-space-between', [
h('label.font-small', 'AMOUNT'),
h('span.font-small', formatBalance(txParams.value)),
]),
this.miniAccountPanelForRecipient(),
]),
])
)
h('style', `
.table-box {
margin: 7px 0px 0px 0px;
width: 100%;
}
.table-box .row {
margin: 0px;
background: rgb(236,236,236);
display: flex;
justify-content: space-between;
font-family: Montserrat Light, sans-serif;
font-size: 13px;
padding: 5px 25px;
}
.table-box .row .value {
font-family: Montserrat Regular;
}
`),
h('.table-box', [
h('.row', [
h('.cell.label', 'Amount'),
h('.cell.value', formatBalance(txParams.value)),
]),
h('.cell.row', [
h('.cell.label', 'Max Transaction Fee'),
h('.cell.value', formatBalance(gasCost)),
]),
h('.cell.row', {
style: {
fontFamily: 'Montserrat Regular',
background: 'white',
padding: '10px 25px',
},
}, [
h('.cell.label', 'Max Total'),
h('.cell.value', {
style: {
display: 'flex',
alignItems: 'center',
},
}, [
h(EtherBalance, {
value: maxCost,
inline: true,
labelColor: 'black',
fontSize: '16px',
}),
]),
]),
h('.cell.row', {
style: {
background: '#f7f7f7',
paddingBottom: '0px',
},
}, [
h('.cell.label'),
h('.cell.value', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '11px',
},
}, `Data included: ${dataLength} bytes`),
]),
]), // End of Table
this.warnIfNeeded(),
])
)
}
PTXP.miniAccountPanelForRecipient = function () {
var props = this.props
var txData = props.txData
var txParams = txData.txParams || {}
var isContractDeploy = !('to' in txParams)
// If it's not a contract deploy, send to the account
if (!isContractDeploy) {
return h(MiniAccountPanel, {
imageSeed: txParams.to,
imageifyIdenticons: props.imageifyIdenticons,
picOrder: 'left',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, nameForAddress(txParams.to, props.identities)),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, addressSummary(txParams.to, 6, 4, false)),
])
} else {
return h(MiniAccountPanel, {
imageifyIdenticons: props.imageifyIdenticons,
picOrder: 'left',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, 'New Contract'),
])
}
}
// Should analyze if there is a DELEGATECALL opcode
// in the recipient contract, and show a warning if so.
PTXP.warnIfNeeded = function () {
const containsDelegateCall = !!this.props.txData.containsDelegateCall
if (!containsDelegateCall) {
return null
}
return h('span.error', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
display: 'flex',
justifyContent: 'center',
},
}, [
h('i.fa.fa-lg.fa-info-circle', { style: { margin: '5px' } }),
h('span', ' Your identity may be used in other contracts!'),
])
}

View File

@ -21,29 +21,35 @@ PendingTx.prototype.render = function () {
key: txData.id,
}, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, 'Submit Transaction'),
// tx info
h(PendingTxDetails, state),
h('style', `
.conf-buttons button {
margin-left: 10px;
text-transform: uppercase;
}
`),
// send + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelTransaction,
}, 'Reject'),
h('button', {
h('.flex-row.flex-space-around.conf-buttons', {
style: {
display: 'flex',
justifyContent: 'flex-end',
margin: '14px 25px',
},
}, [
h('button.confirm', {
onClick: state.sendTransaction,
}, 'Approve'),
style: { background: 'rgb(251,117,1)' },
}, 'Accept'),
h('button.cancel', {
onClick: state.cancelTransaction,
style: { background: 'rgb(254,35,17)' },
}, 'Reject'),
]),
])
)
}

View File

@ -39,14 +39,14 @@ ConfirmTxScreen.prototype.render = function () {
return (
h('.unconftx-section.flex-column.flex-grow', [
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.goHome.bind(this),
}),
h('h2.page-subtitle', 'Confirmation'),
h('h2.page-subtitle', 'Confirm Transaction'),
]),
h('h3', {

View File

@ -411,10 +411,6 @@ input.large-input {
}
/* tx confirm */
.unconftx-section {
margin: 0 20px;
}
.unconftx-section input[type=password] {
height: 22px;
padding: 2px;

View File

@ -220,3 +220,9 @@ hr.horizontal-line {
.invisible {
visibility: hidden;
}
.one-line-concat {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -21,6 +21,7 @@ for (var currency in valueTable) {
module.exports = {
valuesFor: valuesFor,
addressSummary: addressSummary,
miniAddressSummary: miniAddressSummary,
isAllOneCase: isAllOneCase,
isValidAddress: isValidAddress,
numericBalance: numericBalance,
@ -44,10 +45,19 @@ function valuesFor (obj) {
.map(function (key) { return obj[key] })
}
function addressSummary (address) {
function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) {
if (!address) return ''
let checked = ethUtil.toChecksumAddress(address)
if (!includeHex) {
checked = ethUtil.stripHexPrefix(checked)
}
return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...'
}
function miniAddressSummary (address) {
if (!address) return ''
var checked = ethUtil.toChecksumAddress(address)
return checked ? checked.slice(0, 2 + 8) + '...' + checked.slice(-4) : '...'
return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...'
}
function isValidAddress (address) {
@ -95,7 +105,8 @@ function parseBalance (balance) {
return [beforeDecimal, afterDecimal]
}
// Takes wei hex, returns "None" or "${formattedAmount} ETH"
// Takes wei hex, returns an object with three properties.
// Its "formatted" property is what we generally use to render values.
function formatBalance (balance, decimalsToKeep) {
var parsed = parseBalance(balance)
var beforeDecimal = parsed[0]

31
ui/lib/contract-namer.js Normal file
View File

@ -0,0 +1,31 @@
/* CONTRACT NAMER
*
* Takes an address,
* Returns a nicname if we have one stored,
* otherwise returns null.
*/
// Nickname keys must be stored in lower case.
const nicknames = {}
module.exports = function(addr, identities = {}) {
const address = addr.toLowerCase()
const ids = hashFromIdentities(identities)
console.dir({ addr, ids })
return addrFromHash(address, ids) || addrFromHash(address, nicknames)
}
function hashFromIdentities(identities) {
const result = {}
for (let key in identities) {
result[key] = identities[key].name
}
return result
}
function addrFromHash(addr, hash) {
const address = addr.toLowerCase()
return hash[address] || null
}